apis_v1.py 7.1 KB

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