From 7a0a65bee4c2c21f78e85c0241714b46b3ebf416 Mon Sep 17 00:00:00 2001 From: Eva Date: Thu, 21 Mar 2024 23:05:33 +0100 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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"] ) From d03c4c7b5ee533dbfc42ab8a5dcaea9a97b895a6 Mon Sep 17 00:00:00 2001 From: ItsKaa <31796925+ItsKaa@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:28:06 +0100 Subject: [PATCH 5/6] server: docker setup for embeds, sharing client's /var/www to the server --- docker-compose.yml | 8 ++++++++ server/config.yaml.dist | 5 +++++ server/szurubooru/api/embed_api.py | 14 ++++++++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 38e08b97..3570d8a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,8 +23,12 @@ services: #LOG_SQL: 0 (1 for verbose SQL logs) THREADS: volumes: + - client:/opt/app/client:ro - "${MOUNT_DATA}:/data" - "./server/config.yaml:/opt/app/config.yaml" + ## Expose this port if you want embeds + #ports: + # - "${PORT_SERVER}:6666" client: image: szurubooru/client:latest @@ -34,6 +38,7 @@ services: BACKEND_HOST: server BASE_URL: volumes: + - client:/var/www - "${MOUNT_DATA}:/data:ro" ports: - "${PORT}:80" @@ -46,3 +51,6 @@ services: POSTGRES_PASSWORD: volumes: - "${MOUNT_SQL}:/var/lib/postgresql/data" + +volumes: + client: diff --git a/server/config.yaml.dist b/server/config.yaml.dist index 222ec06a..6c992965 100644 --- a/server/config.yaml.dist +++ b/server/config.yaml.dist @@ -172,11 +172,16 @@ privileges: homepage_url: https://www.example.com/ site_url: https://www.example.com/booru +## Client folder sharing, required for embeds. +# Docker requires you to share /var/www from the client to the server. +client_dir: /opt/app/client + ## ONLY SET THESE IF DEPLOYING OUTSIDE OF DOCKER #debug: 0 # generate server logs? #show_sql: 0 # show sql in server logs? #data_url: /data/ #data_dir: /var/www/data +#client_dir: /var/www ## usage: schema://user:password@host:port/database_name ## example: postgres://szuru:dog@localhost:5432/szuru_test #database: diff --git a/server/szurubooru/api/embed_api.py b/server/szurubooru/api/embed_api.py index d2a7b577..6ee12fb4 100644 --- a/server/szurubooru/api/embed_api.py +++ b/server/szurubooru/api/embed_api.py @@ -1,3 +1,5 @@ +import logging +from pathlib import Path import re import html from urllib.parse import quote @@ -10,8 +12,11 @@ from szurubooru.func import ( serialization, ) -with open(f"{config.config['data_dir']}/../index.htm") as index: - index_html = index.read() +if (Path(config.config['client_dir']) / "index.htm").exists(): + with open(f"{config.config['client_dir']}/index.htm") as index: + index_html = index.read() +else: + logging.warning("Could not find index.htm needed for embeds.") def _index_path(params: Dict[str, str]) -> int: try: @@ -78,6 +83,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) + + if not index_html: + logging.info("Embed was requested but index.htm file does not exist. Redirecting to 404.") + return {"return_type": "custom", "status_code": "404", "content": [("content-type", "text/html")]} + try: oembed = get_post(ctx, {}, path) except posts.PostNotFoundError: From 40049b6fe280773bb31f2abeedbe069b2f586e4f Mon Sep 17 00:00:00 2001 From: hujle <16465620+hujle@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:32:18 +0300 Subject: [PATCH 6/6] server: docker port exposure fix for embeds --- doc/example.env | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/example.env b/doc/example.env index 303a25e6..6696a4eb 100644 --- a/doc/example.env +++ b/doc/example.env @@ -10,6 +10,10 @@ BUILD_INFO=latest # otherwise the port specified here will be publicly accessible PORT=8080 +# Uncomment PORT_SERVER variable if you did the same in docker-compose.yml +# to make embeds work. +# PORT_SERVER=8081 + # How many waitress threads to start # 4 is the default amount of threads. If you experience performance # degradation with a large number of posts, increasing this may