server_controller.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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': 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. try:
  75. if json_request['limit'] == '':
  76. limit = None
  77. elif json_request['limit'] is None:
  78. limit = None
  79. else:
  80. if buy:
  81. limit = floor(float(json_request['limit']) * 10000) / 10000
  82. else:
  83. limit = ceil(float(json_request['limit']) * 10000) / 10000
  84. except ValueError: # for example when float fails
  85. return BadRequest('Invalid limit.')
  86. except KeyError: # for example when limit was not specified
  87. limit = None
  88. if limit is not None and limit < 0:
  89. return BadRequest('Limit must not be negative.')
  90. if 'stop_loss' in json_request:
  91. if json_request['stop_loss'] == '':
  92. stop_loss = None
  93. elif json_request['stop_loss'] is None:
  94. stop_loss = None
  95. else:
  96. stop_loss = json_request['stop_loss']
  97. else:
  98. stop_loss = None
  99. if stop_loss and limit is None:
  100. return BadRequest('You need to specify a limit for stop-loss orders')
  101. if ioc and stop_loss:
  102. raise BadRequest('Stop loss orders can not be IOC orders.')
  103. if sell:
  104. if not model.user_has_at_least_available(amount, user_id, ownable_id):
  105. return BadRequest('You can not sell more than you own (this also takes into account existing '
  106. 'sell orders and, if you are a bank, required minimum reserves at the ).')
  107. try:
  108. expiry = model.current_db_timestamp() + timedelta(minutes=time_until_expiration).total_seconds()
  109. except OverflowError:
  110. return BadRequest('The expiration time is too far in the future.')
  111. model.place_order(buy, ownership_id, limit, stop_loss, amount, expiry, ioc)
  112. return {'message': "Order placed."}
  113. def gift(json_request):
  114. check_missing_attributes(json_request, ['session_id', 'amount', 'object_name', 'username'])
  115. if not model.ownable_name_exists(json_request['object_name']):
  116. return BadRequest('This kind of object can not be given away.')
  117. if json_request['username'] == BANK_NAME or not model.user_exists(json_request['username']):
  118. return BadRequest('There is no user with this name.')
  119. try:
  120. amount = float(json_request['amount'])
  121. except ValueError:
  122. return BadRequest('Invalid amount.')
  123. ownable_id = model.ownable_id_by_name(json_request['object_name'])
  124. sender_id = model.get_user_id_by_session_id(json_request['session_id'])
  125. if model.user_available_ownable(sender_id, ownable_id) == 0:
  126. return BadRequest('You do not own any of these.')
  127. if not model.user_has_at_least_available(amount, sender_id, ownable_id):
  128. # for example if you have a 1.23532143213 Kollar and want to give them all away
  129. amount = model.user_available_ownable(sender_id, ownable_id)
  130. recipient_id = model.get_user_id_by_name(json_request['username'])
  131. model.send_ownable(sender_id,
  132. recipient_id,
  133. ownable_id,
  134. amount)
  135. return {'message': f"Sent {amount} {model.ownable_name_by_id(ownable_id)} to {model.user_name_by_id(recipient_id)}."}
  136. def orders(json_request):
  137. check_missing_attributes(json_request, ['session_id'])
  138. data = model.get_user_orders(model.get_user_id_by_session_id(json_request['session_id']))
  139. return {'data': data}
  140. def loans(json_request):
  141. check_missing_attributes(json_request, ['session_id'])
  142. data = model.get_user_loans(model.get_user_id_by_session_id(json_request['session_id']))
  143. return {'data': data}
  144. def credits(json_request):
  145. if 'issuer' in json_request:
  146. issuer_id = model.get_user_id_by_name(json_request['issuer'])
  147. else:
  148. issuer_id = None
  149. if 'only_next_mro_qualified' in json_request:
  150. only_next_mro_qualified = json_request['only_next_mro_qualified']
  151. if isinstance(only_next_mro_qualified, str):
  152. raise BadRequest
  153. else:
  154. only_next_mro_qualified = False
  155. data = model.credits(issuer_id, only_next_mro_qualified)
  156. return {'data': data}
  157. def orders_on(json_request):
  158. check_missing_attributes(json_request, ['session_id', 'ownable'])
  159. if not model.ownable_name_exists(json_request['ownable']):
  160. return BadRequest('This kind of object can not be ordered.')
  161. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  162. ownable_id = model.ownable_id_by_name(json_request['ownable'])
  163. data = model.get_ownable_orders(user_id, ownable_id)
  164. return {'data': data}
  165. def old_orders(json_request):
  166. check_missing_attributes(json_request, ['session_id', 'include_canceled', 'include_executed', 'limit'])
  167. include_executed = json_request['include_executed']
  168. include_canceled = json_request['include_canceled']
  169. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  170. limit = json_request['limit']
  171. data = model.get_old_orders(user_id, include_executed, include_canceled, limit)
  172. return {'data': data}
  173. def cancel_order(json_request):
  174. check_missing_attributes(json_request, ['session_id', 'order_id'])
  175. if not model.user_has_order_with_id(json_request['session_id'], json_request['order_id']):
  176. return BadRequest('You do not have an order with that number.')
  177. model.delete_order(json_request['order_id'], 'Canceled')
  178. return {'message': "Successfully deleted order"}
  179. def change_password(json_request):
  180. check_missing_attributes(json_request, ['session_id', 'password'])
  181. salt = str(uuid.uuid4())
  182. hashed_password = sha256_crypt.encrypt(json_request['password'] + salt)
  183. model.change_password(json_request['session_id'], hashed_password, salt)
  184. model.sign_out_user(json_request['session_id'])
  185. return {'message': "Successfully changed password"}
  186. def logout(json_request):
  187. check_missing_attributes(json_request, ['session_id'])
  188. model.sign_out_user(json_request['session_id'])
  189. return {'message': "Successfully logged out"}
  190. def buy_banking_license(json_request):
  191. check_missing_attributes(json_request, ['session_id'])
  192. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  193. if model.user_has_banking_license(user_id):
  194. raise PreconditionFailed('You already have a banking license.')
  195. price = model.global_control_value('banking_license_price')
  196. if model.user_available_money(user_id) < price:
  197. raise PreconditionFailed('You do not have enough money.')
  198. model.send_ownable(user_id, model.bank_id(), model.currency_id(), price)
  199. model.assign_banking_licence(user_id)
  200. return {'message': "Successfully bought banking license"}
  201. def news(_json_request):
  202. return {'data': model.news()}
  203. def tender_calendar(_json_request):
  204. return {'data': model.tender_calendar()}
  205. def tradables(_json_request):
  206. return {'data': model.ownables()}
  207. def trades(json_request):
  208. check_missing_attributes(json_request, ['session_id', 'limit'])
  209. return {'data': model.trades(model.get_user_id_by_session_id(json_request['session_id']), json_request['limit'])}
  210. def trades_on(json_request):
  211. check_missing_attributes(json_request, ['session_id', 'ownable', 'limit'])
  212. if not model.ownable_name_exists(json_request['ownable']):
  213. return BadRequest('This kind of object can not have transactions.')
  214. return {'data': model.trades_on(model.ownable_id_by_name(json_request['ownable']), json_request['limit'])}
  215. def leaderboard(_json_request):
  216. return {'data': model.leaderboard()}
  217. def take_out_personal_loan(json_request):
  218. check_missing_attributes(json_request, ['session_id', 'amount', ])
  219. amount = json_request['amount']
  220. if not isinstance(amount, float) or amount <= 0:
  221. raise BadRequest('Amount must be a number larger than 0')
  222. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  223. model.take_out_personal_loan(user_id, amount)
  224. return {'message': "Successfully took out personal loan"}
  225. def issue_bond(json_request):
  226. check_missing_attributes(json_request, ['session_id', 'name', 'coupon', 'run_time'])
  227. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  228. coupon = json_request['coupon']
  229. if coupon == 'next_mro':
  230. coupon = model.next_mro_interest()
  231. else:
  232. try:
  233. coupon = float(coupon)
  234. except ValueError:
  235. raise BadRequest('Coupon must be a number.')
  236. ownable_name = json_request['name']
  237. if not re.fullmatch(OWNABLE_NAME_PATTERN, ownable_name):
  238. raise BadRequest('Invalid name.')
  239. run_time = json_request['run_time']
  240. if run_time == 'next_mro':
  241. maturity_dt = model.next_mro_dt()
  242. else:
  243. try:
  244. run_time = int(run_time)
  245. except ValueError:
  246. raise BadRequest('Run-time must be a positive integer number.')
  247. if run_time < 0:
  248. raise BadRequest('Run-time must be a positive integer number.')
  249. maturity_dt = model.current_db_timestamp() + 60 * run_time
  250. model.issue_bond(user_id, ownable_name, coupon, maturity_dt)
  251. return {'message': "Successfully issued bond"}
  252. def repay_loan(json_request):
  253. check_missing_attributes(json_request, ['session_id', 'amount', 'loan_id'])
  254. amount = json_request['amount']
  255. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  256. loan_id = json_request['loan_id']
  257. if not model.user_has_loan_with_id(user_id, loan_id, ):
  258. raise NotFound('Unknown loan ID.')
  259. if amount == 'all':
  260. amount = model.loan_remaining_amount(loan_id)
  261. if amount < 0:
  262. raise BadRequest('You can not repay negative amounts.')
  263. if model.user_available_money(user_id) < amount:
  264. if model.user_has_banking_license(user_id):
  265. raise PreconditionFailed('You do not have enough money. '
  266. 'If you are a bank this also takes into account the minimum reserve you need to keep at the central bank.')
  267. else:
  268. raise PreconditionFailed('You do not have enough money.')
  269. if not model.loan_id_exists(loan_id) or model.loan_recipient_id(loan_id) != user_id:
  270. raise NotFound(f'You do not have a loan with that id.')
  271. loan_volume = model.loan_remaining_amount(loan_id)
  272. if loan_volume < amount:
  273. raise PreconditionFailed(f'You can not repay more than the remaining loan volume of {loan_volume}.')
  274. model.repay_loan(loan_id, amount, known_user_id=user_id)
  275. return {'message': "Successfully repayed loan"}
  276. def server_version(_json_request):
  277. return {'version': version.__version__}
  278. def _before_request(_json_request):
  279. # update tender calendar
  280. model.update_tender_calendar()
  281. for mro_id, expiry, min_interest, mro_dt in model.triggered_mros():
  282. # pay interest rates for loans until this mro
  283. model.pay_loan_interest(until=mro_dt)
  284. # pay interest rates for credits until this mro
  285. model.pay_bond_interest(until=mro_dt)
  286. # pay deposit facility for minimum reserves until this mro
  287. model.pay_deposit_facility(until=mro_dt)
  288. # handle MROs
  289. model.mro(mro_id, expiry, min_interest)
  290. # pay interest rates for loans until current time
  291. model.pay_loan_interest()
  292. # pay interest rates for credits until current time
  293. model.pay_bond_interest()
  294. # pay deposit facility for minimum reserves until current time
  295. model.pay_deposit_facility()