web.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import flask
  2. import pystache
  3. import lc.config as c
  4. import lc.error as e
  5. import lc.model as m
  6. class Endpoint:
  7. def __init__(self):
  8. self.user = None
  9. # try finding the token
  10. token = None
  11. # first check the HTTP headers
  12. if (auth := flask.request.headers.get("Authorization", None)) :
  13. token = auth.split()[1]
  14. # if that fails, check the session
  15. elif flask.session.get("auth", None):
  16. token = flask.session["auth"]
  17. # if that exists and we can deserialize it, then make sure
  18. # it contains a valid user password, too
  19. if token and (payload := c.SERIALIZER.loads(token)):
  20. if "name" not in payload or "password" not in payload:
  21. return
  22. try:
  23. u = m.User.by_slug(payload["name"])
  24. except e.LCException:
  25. return
  26. if u.authenticate(payload["password"]):
  27. self.user = u
  28. def request_data(self, cls):
  29. if flask.request.content_type == "application/json":
  30. return cls.from_json(flask.request.data)
  31. elif flask.request.content_type == "application/x-www-form-urlencoded":
  32. return cls.from_form(flask.request.form)
  33. else:
  34. raise e.BadContentType(flask.request.content_type)
  35. def require_authentication(self, name: str) -> m.User:
  36. """
  37. Check that the currently logged-in user exists and is the
  38. same as the user whose username is given. Raises an exception
  39. otherwise.
  40. """
  41. if not self.user or name != self.user.name:
  42. raise e.BadPermissions()
  43. return self.user
  44. def route(self, *args, **kwargs):
  45. """Forward to the appropriate routing method"""
  46. try:
  47. if flask.request.method == "POST":
  48. # all POST methods are "API methods": if we want to
  49. # display information in response to a post, then we
  50. # should redirect to the page where that information
  51. # can be viewed instead of returning that
  52. # information. (I think.)
  53. return flask.jsonify(self.api_post(*args, **kwargs))
  54. elif (
  55. flask.request.method in ["GET", "HEAD"]
  56. and flask.request.content_type == "application/json"
  57. ):
  58. # Here we're distinguishing between an API GET (i.e. a
  59. # client trying to get JSON data about an endpoint)
  60. # versus a user-level GET (i.e. a user in a browser.)
  61. # I like using the HTTP headers to distinguish these
  62. # cases, while other APIs tend to have a separate /api
  63. # endpoint to do this.
  64. return flask.jsonify(self.api_get(*args, **kwargs))
  65. # if an exception arose from an "API method", then we should
  66. # report it as JSON
  67. except e.LCException as exn:
  68. return ({"status": exn.http_code(), "error": str(exn)}, exn.http_code())
  69. # also maybe we tried to redirect, so just do that
  70. except e.LCRedirect as exn:
  71. return flask.redirect(exn.to_path())
  72. # if we're here, it means we're just trying to get a typical
  73. # HTML request.
  74. try:
  75. return self.html(*args, **kwargs)
  76. except e.LCException as exn:
  77. page = render(
  78. "main", title="error", content=f"shit's fucked yo: {exn}", user=None,
  79. )
  80. return (page, exn.http_code())
  81. except e.LCRedirect as exn:
  82. return flask.redirect(exn.to_path())
  83. # Decorators result in some weird code in Python, especially 'cause it
  84. # doesn't make higher-order functions terse. Let's break this down a
  85. # bit. This out method, `endpoint`, takes the route...
  86. def endpoint(route: str):
  87. """Route an endpoint using our semi-smart routing machinery"""
  88. # but `endpoint` returns another function which is going to be
  89. # called with the result of the definition after it. The argument
  90. # to what we're calling `do_endpoint` here is going to be the
  91. # class object defined afterwards.
  92. def do_endpoint(endpoint_class):
  93. # we'll just make that explicit here
  94. assert Endpoint in endpoint_class.__bases__
  95. # finally, we need a function that we'll give to Flask in
  96. # order to actually dispatch to. This is the actual routing
  97. # function, which is why it just creates an instance of the
  98. # endpoint provided above and calls the `route` method on it
  99. def func(*args, **kwargs):
  100. return endpoint_class().route(*args, **kwargs)
  101. # use reflection over the methods defined by the endpoint
  102. # class to decide if it needs to accept POST requests or not.
  103. methods = ["GET"]
  104. if "api_post" in dir(cls):
  105. methods.append("POST")
  106. # this is just for making error messages nicer
  107. func.__name__ = cls.__name__
  108. # finally, use the Flask routing machinery to register our callback
  109. return c.app.route(route, methods=methods)(func)
  110. return do_endpoint
  111. LOADER = pystache.loader.Loader(extension="mustache", search_dirs=["templates"])
  112. def render(name, **kwargs):
  113. """Load and use a Mustache template from the project root"""
  114. template = LOADER.load_name(name)
  115. renderer = pystache.Renderer(missing_tags="strict", search_dirs=["templates"])
  116. return renderer.render(template, kwargs)