7 Commits

Author SHA1 Message Date
Eva
181f2e1828 Merge e59beb4670 into 376f687c38 2025-02-25 16:19:23 +01:00
376f687c38 chore: questionable is not a recognized rating 2025-02-11 21:50:27 +01:00
4fd848abf2 doc: use docker compose instead of docker-compose
The minimum version requirements are rough guesses, in practice any decently modern docker installation should work.
2025-02-11 21:25:10 +01:00
Eva
e59beb4670 server/embed: only serialize post data we actually use 2024-03-25 13:46:38 +01:00
Eva
922499cb64 server/embed: return 404 on post not found 2024-03-21 23:05:34 +01:00
Eva
a88e73804c server/embed: return html on index error 2024-03-21 23:05:34 +01:00
Eva
7a0a65bee4 server/api: add oEmbed and Open Graph 2024-03-21 23:05:33 +01:00
7 changed files with 123 additions and 15 deletions

View File

@ -789,7 +789,7 @@ data.
| `fav-time` | alias of `fav-date` |
| `feature-date` | featured at given date |
| `feature-time` | alias of `feature-time` |
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` (or `questionable`) or `unsafe`. |
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` or `unsafe`. |
| `rating` | alias of `safety` |
**Sort style tokens**

View File

@ -1,5 +1,5 @@
This assumes that you have Docker (version 17.05 or greater)
and Docker Compose (version 1.6.0 or greater) already installed.
This assumes that you have Docker (version 19.03 or greater)
and the Docker Compose CLI (version 1.27.0 or greater) already installed.
### Prepare things
@ -38,7 +38,7 @@ and Docker Compose (version 1.6.0 or greater) already installed.
This pulls the latest containers from docker.io:
```console
user@host:szuru$ docker-compose pull
user@host:szuru$ docker compose pull
```
If you have modified the application's source and would like to manually
@ -49,17 +49,17 @@ and Docker Compose (version 1.6.0 or greater) already installed.
For first run, it is recommended to start the database separately:
```console
user@host:szuru$ docker-compose up -d sql
user@host:szuru$ docker compose up -d sql
```
To start all containers:
```console
user@host:szuru$ docker-compose up -d
user@host:szuru$ docker compose up -d
```
To view/monitor the application logs:
```console
user@host:szuru$ docker-compose logs -f
user@host:szuru$ docker compose logs -f
# (CTRL+C to exit)
```
@ -84,13 +84,13 @@ and Docker Compose (version 1.6.0 or greater) already installed.
2. Build the containers:
```console
user@host:szuru$ docker-compose build
user@host:szuru$ docker compose build
```
That will attempt to build both containers, but you can specify `client`
or `server` to make it build only one.
If `docker-compose build` spits out:
If `docker compose build` spits out:
```
ERROR: Service 'server' failed to build: failed to parse platform : "" is an invalid component of "": platform specifier component must match "^[A-Za-z0-9_-]+$": invalid argument
@ -102,7 +102,7 @@ and Docker Compose (version 1.6.0 or greater) already installed.
user@host:szuru$ export DOCKER_BUILDKIT=1; export COMPOSE_DOCKER_CLI_BUILD=1
```
...and run `docker-compose build` again.
...and run `docker compose build` again.
*Note: If your changes are not taking effect in your builds, consider building
with `--no-cache`.*
@ -117,7 +117,7 @@ with `--no-cache`.*
run from docker:
```console
user@host:szuru$ docker-compose run server ./szuru-admin --help
user@host:szuru$ docker compose run server ./szuru-admin --help
```
will give you a breakdown on all available commands.

View File

@ -1,9 +1,7 @@
## Example Docker Compose configuration
##
## Use this as a template to set up docker-compose, or as guide to set up other
## Use this as a template to set up docker compose, or as guide to set up other
## orchestration services
version: '2'
services:
server:

View File

@ -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?

View File

@ -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

View File

@ -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<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)
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]:
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"),)