This commit is contained in:
Eva
2025-05-28 17:39:48 +02:00
committed by GitHub
6 changed files with 138 additions and 1 deletions

View File

@ -10,6 +10,10 @@ BUILD_INFO=latest
# otherwise the port specified here will be publicly accessible # otherwise the port specified here will be publicly accessible
PORT=8080 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 # How many waitress threads to start
# 4 is the default amount of threads. If you experience performance # 4 is the default amount of threads. If you experience performance
# degradation with a large number of posts, increasing this may # degradation with a large number of posts, increasing this may

View File

@ -21,8 +21,12 @@ services:
#LOG_SQL: 0 (1 for verbose SQL logs) #LOG_SQL: 0 (1 for verbose SQL logs)
THREADS: THREADS:
volumes: volumes:
- client:/opt/app/client:ro
- "${MOUNT_DATA}:/data" - "${MOUNT_DATA}:/data"
- "./server/config.yaml:/opt/app/config.yaml" - "./server/config.yaml:/opt/app/config.yaml"
## Expose this port if you want embeds
#ports:
# - "${PORT_SERVER}:6666"
client: client:
image: szurubooru/client:latest image: szurubooru/client:latest
@ -32,6 +36,7 @@ services:
BACKEND_HOST: server BACKEND_HOST: server
BASE_URL: BASE_URL:
volumes: volumes:
- client:/var/www
- "${MOUNT_DATA}:/data:ro" - "${MOUNT_DATA}:/data:ro"
ports: ports:
- "${PORT}:80" - "${PORT}:80"
@ -44,3 +49,6 @@ services:
POSTGRES_PASSWORD: POSTGRES_PASSWORD:
volumes: volumes:
- "${MOUNT_SQL}:/var/lib/postgresql/data" - "${MOUNT_SQL}:/var/lib/postgresql/data"
volumes:
client:

View File

@ -169,11 +169,19 @@ privileges:
'uploads:create': regular 'uploads:create': regular
'uploads:use_downloader': power 'uploads:use_downloader': power
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 ## ONLY SET THESE IF DEPLOYING OUTSIDE OF DOCKER
#debug: 0 # generate server logs? #debug: 0 # generate server logs?
#show_sql: 0 # show sql in server logs? #show_sql: 0 # show sql in server logs?
#data_url: /data/ #data_url: /data/
#data_dir: /var/www/data #data_dir: /var/www/data
#client_dir: /var/www
## usage: schema://user:password@host:port/database_name ## usage: schema://user:password@host:port/database_name
## example: postgres://szuru:dog@localhost:5432/szuru_test ## example: postgres://szuru:dog@localhost:5432/szuru_test
#database: #database:

View File

@ -1,4 +1,5 @@
import szurubooru.api.comment_api import szurubooru.api.comment_api
import szurubooru.api.embed_api
import szurubooru.api.info_api import szurubooru.api.info_api
import szurubooru.api.password_reset_api import szurubooru.api.password_reset_api
import szurubooru.api.pool_api import szurubooru.api.pool_api

View File

@ -0,0 +1,111 @@
import logging
from pathlib import Path
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,
)
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:
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<post_id>\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<path>/.+)")
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:
return {"return_type": "custom", "status_code": "404", "content": index_html}
url = config.config["site_url"] + path
new_html = index_html.replace("</head>", f'''
<meta property="og:site_name" content="{config.config["name"]}">
<meta property="og:url" content="{html.escape(url)}">
<meta property="og:type" content="article">
<meta property="og:title" content="{html.escape(oembed['title'])}">
<meta name="twitter:title" content="{html.escape(oembed['title'])}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="{html.escape(oembed['url'])}">
<meta property="og:image:url" content="{html.escape(oembed['url'])}">
<meta property="og:image:width" content="{oembed['width']}">
<meta property="og:image:height" content="{oembed['height']}">
<meta property="article:author" content="{html.escape(oembed['author_name'] or '')}">
<link rel="alternate" type="application/json+oembed" href="{config.config["site_url"]}/api/oembed?url={quote(html.escape(url))}" title="{html.escape(config.config["name"])}"></head>
''').replace("<html>", '<html prefix="og: http://ogp.me/ns#">').replace("<title>Loading...</title>", f"<title>{html.escape(oembed['title'])}</title>")
return {"return_type": "custom", "content": new_html}

View File

@ -74,7 +74,8 @@ def application(
) -> Tuple[bytes]: ) -> Tuple[bytes]:
try: try:
ctx = _create_context(env) 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( raise errors.HttpNotAcceptable(
"ValidationError", "This API only supports JSON responses." "ValidationError", "This API only supports JSON responses."
) )
@ -111,6 +112,10 @@ def application(
finally: finally:
db.session.remove() 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")]) start_response("200", [("content-type", "application/json")])
return (_dump_json(response).encode("utf-8"),) return (_dump_json(response).encode("utf-8"),)