apis_v1.py 7.6 KB

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