apis_v1.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. """
  2. 新的 RESTful API
  3. """
  4. import json
  5. from homepagebuilder.interfaces import require
  6. from homepagebuilder.interfaces.Events import on
  7. from homepagebuilder.core.types import Context
  8. from typing import NamedTuple
  9. mcv = require('minecraft_versions') # 需求前置 MinecraftVersions
  10. MCVM = mcv.MCVM
  11. rest_apis_routes = []
  12. # region API Routes
  13. def api(path, version=None, methods=None):
  14. if version is None:
  15. version = 'v1'
  16. if methods is None:
  17. methods = ['GET']
  18. if path[0] != '/':
  19. path = '/' + path
  20. def deco(cls):
  21. api_object = cls()
  22. rest_apis_routes.append((f'/{version}{path}', f'api.{version}.{cls.__name__}',
  23. api_object.generate, methods))
  24. return deco
  25. @on('server.setup_routes.return')
  26. def set_up_routes(server, *_, **__):
  27. for route in rest_apis_routes:
  28. print('/api' + route[0])
  29. server.app.add_url_rule('/api' + route[0], route[1], route[2], methods=route[3])
  30. # endregion
  31. def is_true(string: str) -> bool:
  32. return bool(string and isinstance(string, str) and string.lower() != 'false')
  33. # region Error Classes
  34. class APIError(Exception):
  35. message = 'Generic Error'
  36. _code = 0
  37. def __init__(self, message):
  38. self.message = message
  39. @property
  40. def code(self):
  41. return self._code
  42. def to_response(self):
  43. return Response(message=self.message, status=self._code)
  44. class BadRequest(APIError):
  45. _code = 400
  46. class Unauthorized(APIError):
  47. _code = 401
  48. class Forbidden(APIError):
  49. _code = 403
  50. class NotFound(APIError):
  51. _code = 404
  52. class InternalServerError(APIError):
  53. _code = 500
  54. # endregion
  55. class RESTAPI:
  56. def process(self, **kwargs) -> tuple[str, dict|list]|str|dict|list:
  57. raise NotImplementedError()
  58. def generate(self, **kwargs):
  59. from flask import request, Response as Flask_Response
  60. response = None
  61. args = request.args
  62. try:
  63. result = self.process(args = args, **kwargs)
  64. if isinstance(result, Response):
  65. response = result
  66. elif isinstance(result, dict | list | str):
  67. response = Response(data = result)
  68. elif isinstance(result, int):
  69. if 200 <= result < 600:
  70. response = Response(status = result)
  71. else:
  72. response = Response(data = str(result))
  73. else:
  74. raise InternalServerError("")
  75. except APIError as e:
  76. response = e.to_response()
  77. except Exception as e:
  78. response = Response(message = "Internal Server Error",
  79. error = {
  80. "type": e.__class__.__name__,
  81. "args": e.args,
  82. },
  83. status = 500)
  84. respond:dict = {'status': response.status,
  85. 'message': response.message}
  86. if response.data:
  87. respond['data'] = response.data
  88. if response.error:
  89. respond['error'] = response.error
  90. ensure_ascii = is_true(args.get('ascii', 'false'))
  91. respond_json = json.dumps(respond, ensure_ascii=ensure_ascii)
  92. return Flask_Response(respond_json, status=response.status, mimetype='application/json' )
  93. class Response(NamedTuple):
  94. data: dict | list | str | None = None
  95. message: str = ''
  96. status: int = 200
  97. error: dict | None = None
  98. @api('/status', 'v1')
  99. class StatusAPI(RESTAPI):
  100. def process(self, **_):
  101. return Response(message='OK')
  102. @api('/mcversion/latest/data', 'v1')
  103. class LatestVersionAPI(RESTAPI):
  104. EXPORT_VERSION_PROPERTIES = [
  105. 'version-id'
  106. 'version-type',
  107. 'title',
  108. 'intro',
  109. 'version-image-link',
  110. 'server-jar',
  111. 'translator',
  112. 'official-link',
  113. 'wiki-link',
  114. ]
  115. def __init__(self):
  116. self.respond = None
  117. self.__inited = False
  118. def init(self):
  119. context = Context.get_current_context()
  120. library = context.project.base_library
  121. self.latest_release = library.get_card(MCVM.get_latest_version('release').id, False)
  122. if latest_developing := MCVM.get_latest_version('development'):
  123. self.latest_developing = library.get_card(latest_developing.id, False)
  124. else:
  125. self.latest_developing = None
  126. self.__inited = True
  127. def generate_respond(self):
  128. if not self.__inited:
  129. self.init()
  130. respond = {}
  131. if self.latest_developing:
  132. respond['snapshot'] = self.extract_version_info(self.latest_developing)
  133. respond['release'] = self.extract_version_info(self.latest_release)
  134. self.respond = respond
  135. def extract_version_info(self, card):
  136. respond = {}
  137. context = Context.get_current_context()
  138. expended_card = context.builder.template_manager.expend_card_placeholders(card, '')
  139. for key in self.EXPORT_VERSION_PROPERTIES:
  140. respond[key] = expended_card.get(key)
  141. respond['homepage-json-link'] = f"https://news.bugjump.net/VersionDetail.json?ver={expended_card['version-id']}"
  142. return respond
  143. def process(self, **_):
  144. if not self.respond:
  145. self.generate_respond()
  146. return Response(data = self.respond)
  147. @api('/mcversion/latest/card', 'v1')
  148. class LatestVersionCardAPI(RESTAPI):
  149. def process(self, **_):
  150. context = Context.get_current_context()
  151. library = context.project.base_library
  152. setter = context.setter
  153. card = library.get_card('VersionLatestListCard', False)
  154. card = setter.decorate(card)
  155. return Response(data = context.builder.template_manager.build(card))
  156. @api('/mcversion/data/<version_id>', 'v1')
  157. class MCVersionDataAPI(RESTAPI):
  158. EXPORT_VERSION_PROPERTIES = [
  159. 'version-id'
  160. 'version-type',
  161. 'title',
  162. 'intro',
  163. 'version-image-link',
  164. 'server-jar',
  165. 'translator',
  166. 'official-link',
  167. 'wiki-link',
  168. 'markdown',
  169. ]
  170. def process(self, version_id, **_):
  171. context = Context.get_current_context()
  172. library = context.project.base_library
  173. card = library.get_card(version_id, False)
  174. if not card:
  175. raise NotFound(f"Version {version_id} not found")
  176. respond = {}
  177. expended_card = context.builder.template_manager.expend_card_placeholders(card, '')
  178. for key in self.EXPORT_VERSION_PROPERTIES:
  179. respond[key] = expended_card.get(key)
  180. return Response(data = respond)
  181. @api('/mcversion/list', 'v1')
  182. class MCVersionListAPI(RESTAPI):
  183. _cache = None
  184. def get_list(self):
  185. context = Context.get_current_context()
  186. base_library = context.project.base_library
  187. assert base_library is not None
  188. version_lib = base_library.get_library('versions')
  189. result = [c["file_name"] for c in version_lib.get_all_cards()]
  190. print(result)
  191. return Response(data = result)
  192. def process(self, **_):
  193. if self._cache is None:
  194. self._cache = self.get_list()
  195. return self._cache
  196. @api('/')
  197. class DefaultNotFoundAPI(RESTAPI):
  198. def process(self, **_):
  199. raise NotFound("")