diff --git a/server/config.yaml.dist b/server/config.yaml.dist index 193aac3a..222ec06a 100644 --- a/server/config.yaml.dist +++ b/server/config.yaml.dist @@ -169,6 +169,9 @@ privileges: 'uploads:create': regular 'uploads:use_downloader': power +homepage_url: https://www.example.com/ +site_url: https://www.example.com/booru + ## ONLY SET THESE IF DEPLOYING OUTSIDE OF DOCKER #debug: 0 # generate server logs? #show_sql: 0 # show sql in server logs? diff --git a/server/szurubooru/api/__init__.py b/server/szurubooru/api/__init__.py index d9b7ecba..854c50cb 100644 --- a/server/szurubooru/api/__init__.py +++ b/server/szurubooru/api/__init__.py @@ -1,4 +1,5 @@ import szurubooru.api.comment_api +import szurubooru.api.embed_api import szurubooru.api.info_api import szurubooru.api.password_reset_api import szurubooru.api.pool_api diff --git a/server/szurubooru/api/embed_api.py b/server/szurubooru/api/embed_api.py new file mode 100644 index 00000000..d2a7b577 --- /dev/null +++ b/server/szurubooru/api/embed_api.py @@ -0,0 +1,101 @@ +import re +import html +from urllib.parse import quote +from typing import Dict, Optional + +from szurubooru import config, model, rest +from szurubooru.func import ( + auth, + posts, + serialization, +) + +with open(f"{config.config['data_dir']}/../index.htm") as index: + index_html = index.read() + +def _index_path(params: Dict[str, str]) -> int: + try: + return params["path"] + except (TypeError, ValueError): + raise posts.InvalidPostIdError( + "Invalid post ID." + ) + + +def _get_post(post_id: int) -> model.Post: + return posts.get_post_by_id(post_id) + + +def _get_post_id(match: re.Match) -> int: + post_id = match.group("post_id") + try: + return int(post_id) + except (TypeError, ValueError): + raise posts.InvalidPostIdError( + "Invalid post ID: %r." % post_id + ) + + +def _serialize_post( + ctx: rest.Context, post: Optional[model.Post] +) -> rest.Response: + return posts.serialize_post( + post, ctx.user, options=["thumbnailUrl", "user"] + ) + + +@rest.routes.get("/oembed/?") +def get_post( + ctx: rest.Context, _params: Dict[str, str] = {}, url: str = "" +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:view") + + url = url or ctx.get_param_as_string("url") + match = re.match(r".*?/post/(?P\d+)", url) + if not match: + raise posts.InvalidPostIdError("Invalid post ID.") + + post_id = _get_post_id(match) + post = _get_post(post_id) + serialized = _serialize_post(ctx, post) + embed = { + "version": "1.0", + "type": "photo", + "title": f"{config.config['name']} – Post #{post_id}", + "author_name": serialized["user"]["name"] if serialized["user"] else None, + "provider_name": config.config["name"], + "provider_url": config.config["homepage_url"], + "thumbnail_url": f"{config.config['site_url']}/{serialized['thumbnailUrl']}", + "thumbnail_width": int(config.config["thumbnails"]["post_width"]), + "thumbnail_height": int(config.config["thumbnails"]["post_height"]), + "url": f"{config.config['site_url']}/{serialized['thumbnailUrl']}", + "width": int(config.config["thumbnails"]["post_width"]), + "height": int(config.config["thumbnails"]["post_height"]) + } + return embed + + +@rest.routes.get("/index(?P/.+)") +def post_index(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + path = _index_path(params) + try: + oembed = get_post(ctx, {}, path) + except posts.PostNotFoundError: + return {"return_type": "custom", "status_code": "404", "content": index_html} + + url = config.config["site_url"] + path + new_html = index_html.replace("", f''' + + + + + + + + + + + + +''').replace("", '').replace("Loading...", f"{html.escape(oembed['title'])}") + return {"return_type": "custom", "content": new_html} diff --git a/server/szurubooru/rest/app.py b/server/szurubooru/rest/app.py index c098bd04..8aa188b5 100644 --- a/server/szurubooru/rest/app.py +++ b/server/szurubooru/rest/app.py @@ -74,7 +74,8 @@ def application( ) -> Tuple[bytes]: try: ctx = _create_context(env) - if "application/json" not in ctx.get_header("Accept"): + accept_header = ctx.get_header("Accept") + if "*/*" not in accept_header and "application/json" not in accept_header: raise errors.HttpNotAcceptable( "ValidationError", "This API only supports JSON responses." ) @@ -111,6 +112,10 @@ def application( finally: db.session.remove() + if type(response) == dict and response.get("return_type") == "custom": + start_response(response.get("status_code", "200"), [("content-type", "text/html")]) + return (response.get("content", "").encode("utf-8"),) + start_response("200", [("content-type", "application/json")]) return (_dump_json(response).encode("utf-8"),)