From 7a0a65bee4c2c21f78e85c0241714b46b3ebf416 Mon Sep 17 00:00:00 2001 From: Eva Date: Thu, 21 Mar 2024 23:05:33 +0100 Subject: [PATCH 1/4] server/api: add oEmbed and Open Graph --- server/config.yaml.dist | 3 + server/szurubooru/api/__init__.py | 1 + server/szurubooru/api/oembed_api.py | 97 +++++++++++++++++++++++++++++ server/szurubooru/rest/app.py | 7 ++- 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 server/szurubooru/api/oembed_api.py 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..f6b8973a 100644 --- a/server/szurubooru/api/__init__.py +++ b/server/szurubooru/api/__init__.py @@ -1,5 +1,6 @@ import szurubooru.api.comment_api import szurubooru.api.info_api +import szurubooru.api.oembed_api import szurubooru.api.password_reset_api import szurubooru.api.pool_api import szurubooru.api.pool_category_api diff --git a/server/szurubooru/api/oembed_api.py b/server/szurubooru/api/oembed_api.py new file mode 100644 index 00000000..cfc9046c --- /dev/null +++ b/server/szurubooru/api/oembed_api.py @@ -0,0 +1,97 @@ +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=serialization.get_serialization_options(ctx) + ) + + +@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) + oembed = get_post(ctx, {}, path) + 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..d95bb145 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("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"),) From a88e73804c1dc18cd5c1d7e0fa603d206d2a2a05 Mon Sep 17 00:00:00 2001 From: Eva Date: Thu, 21 Mar 2024 01:05:55 +0100 Subject: [PATCH 2/4] server/embed: return html on index error --- server/szurubooru/api/__init__.py | 2 +- server/szurubooru/api/{oembed_api.py => embed_api.py} | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) rename server/szurubooru/api/{oembed_api.py => embed_api.py} (96%) diff --git a/server/szurubooru/api/__init__.py b/server/szurubooru/api/__init__.py index f6b8973a..854c50cb 100644 --- a/server/szurubooru/api/__init__.py +++ b/server/szurubooru/api/__init__.py @@ -1,6 +1,6 @@ import szurubooru.api.comment_api +import szurubooru.api.embed_api import szurubooru.api.info_api -import szurubooru.api.oembed_api import szurubooru.api.password_reset_api import szurubooru.api.pool_api import szurubooru.api.pool_category_api diff --git a/server/szurubooru/api/oembed_api.py b/server/szurubooru/api/embed_api.py similarity index 96% rename from server/szurubooru/api/oembed_api.py rename to server/szurubooru/api/embed_api.py index cfc9046c..dbead402 100644 --- a/server/szurubooru/api/oembed_api.py +++ b/server/szurubooru/api/embed_api.py @@ -78,7 +78,11 @@ def get_post( @rest.routes.get("/index(?P/.+)") def post_index(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: path = _index_path(params) - oembed = get_post(ctx, {}, path) + try: + oembed = get_post(ctx, {}, path) + except posts.PostNotFoundError: + return {"return_type": "custom", "content": index_html} + url = config.config["site_url"] + path new_html = index_html.replace("", f''' From 922499cb64552b627916b245957702ac465e2536 Mon Sep 17 00:00:00 2001 From: Eva Date: Thu, 21 Mar 2024 02:23:42 +0100 Subject: [PATCH 3/4] server/embed: return 404 on post not found --- server/szurubooru/api/embed_api.py | 2 +- server/szurubooru/rest/app.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/szurubooru/api/embed_api.py b/server/szurubooru/api/embed_api.py index dbead402..c83a8a50 100644 --- a/server/szurubooru/api/embed_api.py +++ b/server/szurubooru/api/embed_api.py @@ -81,7 +81,7 @@ def post_index(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: try: oembed = get_post(ctx, {}, path) except posts.PostNotFoundError: - return {"return_type": "custom", "content": index_html} + return {"return_type": "custom", "status_code": "404", "content": index_html} url = config.config["site_url"] + path new_html = index_html.replace("", f''' diff --git a/server/szurubooru/rest/app.py b/server/szurubooru/rest/app.py index d95bb145..8aa188b5 100644 --- a/server/szurubooru/rest/app.py +++ b/server/szurubooru/rest/app.py @@ -113,7 +113,7 @@ def application( db.session.remove() if type(response) == dict and response.get("return_type") == "custom": - start_response("200", [("content-type", "text/html")]) + 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")]) From e59beb46708a61bdb1c4af19edb2ec23eb535579 Mon Sep 17 00:00:00 2001 From: Eva Date: Mon, 25 Mar 2024 13:46:38 +0100 Subject: [PATCH 4/4] server/embed: only serialize post data we actually use --- server/szurubooru/api/embed_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/szurubooru/api/embed_api.py b/server/szurubooru/api/embed_api.py index c83a8a50..d2a7b577 100644 --- a/server/szurubooru/api/embed_api.py +++ b/server/szurubooru/api/embed_api.py @@ -40,7 +40,7 @@ def _serialize_post( ctx: rest.Context, post: Optional[model.Post] ) -> rest.Response: return posts.serialize_post( - post, ctx.user, options=serialization.get_serialization_options(ctx) + post, ctx.user, options=["thumbnailUrl", "user"] )