mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Compare commits
6 Commits
33321fd59f
...
772c12eefd
Author | SHA1 | Date | |
---|---|---|---|
772c12eefd | |||
376f687c38 | |||
4fd848abf2 | |||
ec890f1e69 | |||
05074527ed | |||
dfe952ddaf |
@ -84,7 +84,7 @@ function bundleHtml() {
|
|||||||
|
|
||||||
function minifyHtml(html) {
|
function minifyHtml(html) {
|
||||||
return require('html-minifier').minify(html, {
|
return require('html-minifier').minify(html, {
|
||||||
removeComments: true,
|
removeComments: false,
|
||||||
collapseWhitespace: true,
|
collapseWhitespace: true,
|
||||||
conservativeCollapse: true,
|
conservativeCollapse: true,
|
||||||
}).trim();
|
}).trim();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html prefix='og: https://ogp.me/ns#'>
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf-8'/>
|
<meta charset='utf-8'/>
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||||
@ -10,6 +10,7 @@
|
|||||||
<meta name="msapplication-TileImage" content="/img/mstile-150x150.png">
|
<meta name="msapplication-TileImage" content="/img/mstile-150x150.png">
|
||||||
<title>Loading...</title>
|
<title>Loading...</title>
|
||||||
<!-- Base HTML Placeholder -->
|
<!-- Base HTML Placeholder -->
|
||||||
|
<!-- Embed Placeholder -->
|
||||||
<link href='css/app.min.css' rel='stylesheet' type='text/css'/>
|
<link href='css/app.min.css' rel='stylesheet' type='text/css'/>
|
||||||
<link href='css/vendor.min.css' rel='stylesheet' type='text/css'/>
|
<link href='css/vendor.min.css' rel='stylesheet' type='text/css'/>
|
||||||
<link rel='shortcut icon' type='image/png' href='img/favicon.png'/>
|
<link rel='shortcut icon' type='image/png' href='img/favicon.png'/>
|
||||||
|
@ -23,6 +23,10 @@ http {
|
|||||||
server __BACKEND__:6666;
|
server __BACKEND__:6666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upstream frontend {
|
||||||
|
server __FRONTEND__:6667;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80 default_server;
|
listen 80 default_server;
|
||||||
|
|
||||||
@ -71,7 +75,7 @@ http {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
root /var/www;
|
root /var/www;
|
||||||
try_files $uri /index.htm;
|
try_files $uri @frontend;
|
||||||
|
|
||||||
sendfile on;
|
sendfile on;
|
||||||
tcp_nopush on;
|
tcp_nopush on;
|
||||||
@ -81,6 +85,10 @@ http {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location @frontend {
|
||||||
|
proxy_pass http://frontend;
|
||||||
|
}
|
||||||
|
|
||||||
location @unauthorized {
|
location @unauthorized {
|
||||||
return 403 "Unauthorized";
|
return 403 "Unauthorized";
|
||||||
default_type text/plain;
|
default_type text/plain;
|
||||||
|
1
client/requirements.txt
Normal file
1
client/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
requests
|
172
client/server.py
Normal file
172
client/server.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import html
|
||||||
|
import urllib.parse
|
||||||
|
import itertools
|
||||||
|
from typing import Dict, Any, Callable, Tuple, List, Iterable
|
||||||
|
from http import HTTPStatus
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
class HttpStatus:
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return str(int(getattr(HTTPStatus, name)))
|
||||||
|
|
||||||
|
http_status = HttpStatus()
|
||||||
|
|
||||||
|
FRONTEND_WEB_ROOT = Path(os.environ["SZURUBOORU_WEB_ROOT"])
|
||||||
|
|
||||||
|
with open(FRONTEND_WEB_ROOT / "index.htm") as f:
|
||||||
|
INDEX_HTML = f.read()
|
||||||
|
|
||||||
|
BACKEND_BASE_URL = os.environ["SZURUBOORU_BASE_URL"]
|
||||||
|
PUBLIC_BASE_URL = os.environ["SZURUBOORU_PUBLIC_BASE_URL"]
|
||||||
|
|
||||||
|
Metadata = Iterable[Tuple[str, str]]
|
||||||
|
|
||||||
|
def general_embed(server_info: Dict[str, Any]) -> Metadata:
|
||||||
|
yield "og:site_name", server_info["config"]["name"]
|
||||||
|
|
||||||
|
def user_embed(username: str) -> Metadata:
|
||||||
|
yield "og:type", "profile"
|
||||||
|
yield "profile:username", username
|
||||||
|
yield "og:title", username
|
||||||
|
yield "og:image:height", "128"
|
||||||
|
yield "og:image:width", "128"
|
||||||
|
user = requests.get(BACKEND_BASE_URL + "/api/user/" + username).json()
|
||||||
|
yield "og:image:url", urllib.parse.join(PUBLIC_BASE_URL, user["avatarUrl"])
|
||||||
|
|
||||||
|
def _image_embed(post: Dict[str, Any], skip_twitter_player=False) -> Metadata:
|
||||||
|
url = PUBLIC_BASE_URL + post["contentUrl"]
|
||||||
|
|
||||||
|
if post["type"] == "video":
|
||||||
|
prefix = "og:video"
|
||||||
|
yield "og:image", PUBLIC_BASE_URL + post["thumbnailUrl"]
|
||||||
|
if not skip_twitter_player:
|
||||||
|
yield "twitter:card", "player"
|
||||||
|
yield "twitter:player", PUBLIC_BASE_URL + f"/player/{post['id']}"
|
||||||
|
yield "twitter:player:width", str(post["canvasWidth"])
|
||||||
|
yield "twitter:player:height", str(post["canvasHeight"])
|
||||||
|
else:
|
||||||
|
prefix = "og:image"
|
||||||
|
yield "twitter:card", "summary_large_image"
|
||||||
|
|
||||||
|
yield prefix + ":width", str(post["canvasWidth"])
|
||||||
|
yield prefix + ":height", str(post["canvasHeight"])
|
||||||
|
yield prefix, url
|
||||||
|
if BACKEND_BASE_URL.startswith('https://'):
|
||||||
|
yield prefix + ":secure_url", url
|
||||||
|
yield prefix + ":type", post["mimeType"]
|
||||||
|
|
||||||
|
def _author_embed(user: Dict[str, Any]) -> Metadata:
|
||||||
|
yield "article:author", PUBLIC_BASE_URL + "/user/" + user["name"]
|
||||||
|
|
||||||
|
def post_embed(post_id: int, *, skip_twitter_player=False) -> Metadata:
|
||||||
|
post = requests.get(BACKEND_BASE_URL + f"/api/post/{post_id}").json()
|
||||||
|
yield "og:type", "article"
|
||||||
|
yield from _author_embed(post["user"])
|
||||||
|
yield "og:title", post["user"]["name"]
|
||||||
|
if post["tags"]:
|
||||||
|
value = "Tags: " + ", ".join(tag["names"][0] for tag in post["tags"])
|
||||||
|
yield "og:description", value
|
||||||
|
yield "description", value
|
||||||
|
yield from _image_embed(post, skip_twitter_player)
|
||||||
|
|
||||||
|
def homepage_embed(server_info: Dict[str, Any], *, skip_twitter_player=False) -> Metadata:
|
||||||
|
yield "og:title", server_info["config"]["name"]
|
||||||
|
yield "og:type", "website"
|
||||||
|
post = server_info["featuredPost"]
|
||||||
|
if post is not None:
|
||||||
|
yield from _image_embed(post, skip_twitter_player)
|
||||||
|
|
||||||
|
def render_embed(metadata: Metadata) -> str:
|
||||||
|
out = []
|
||||||
|
for k, v in metadata:
|
||||||
|
k, v = html.escape(k), html.escape(v)
|
||||||
|
out.append(f'<meta property="{k}" content="{v}">')
|
||||||
|
return ''.join(out)
|
||||||
|
|
||||||
|
def serve_twitter_video_player(start_response, post_id: int):
|
||||||
|
r = requests.get(BACKEND_BASE_URL + f"/api/post/{post_id}")
|
||||||
|
data = r.json()
|
||||||
|
if r.status_code != HTTPStatus.OK:
|
||||||
|
start_response(r.status_code, [("Content-Type", "text/html; charset=utf-8")])
|
||||||
|
yield f"<h1>{html.escape(data['title'])}</h1><p>{html.escape(data['description'])}</p>".encode("utf-8"),
|
||||||
|
|
||||||
|
start_response(http_status.OK, [("Content-Type", "text/html; charset=utf-8")])
|
||||||
|
post = data
|
||||||
|
yield b"<!DOCTYPE html><html><head><title>​</title>"
|
||||||
|
yield b"<style type='text/css'>video { width: 100%; max-width: 600px; height: auto; }</style></head><body>"
|
||||||
|
yield b"<video autoplay controls"
|
||||||
|
flags = set(post["flags"])
|
||||||
|
if "loop" in flags:
|
||||||
|
yield b" loop"
|
||||||
|
|
||||||
|
if "sound" not in flags:
|
||||||
|
yield b" muted"
|
||||||
|
|
||||||
|
yield f"><source type='{post['mimeType']}' src='{post['contentUrl']}'>Your browser doesn't support HTML5 videos.".encode("utf-8")
|
||||||
|
yield b"</video></body></html>"
|
||||||
|
|
||||||
|
def application(env: Dict[str, Any], start_response: Callable[[str, Any], Any]) -> Tuple[bytes]:
|
||||||
|
def serve_file(path):
|
||||||
|
start_response(http_status.OK, [("X-Accel-Redirect", path)])
|
||||||
|
return ()
|
||||||
|
|
||||||
|
def serve_without_embed():
|
||||||
|
return serve_file("/index.htm")
|
||||||
|
|
||||||
|
method = env["REQUEST_METHOD"]
|
||||||
|
if method != "GET":
|
||||||
|
start_response(http_status.BAD_REQUEST, [("Content-Type", "text/plain")])
|
||||||
|
return (b"Bad request",)
|
||||||
|
|
||||||
|
path = env["PATH_INFO"].lstrip("/")
|
||||||
|
path = path.encode("latin-1").decode("utf-8") # PEP-3333
|
||||||
|
|
||||||
|
if path and (FRONTEND_WEB_ROOT / path).exists():
|
||||||
|
return serve_file("/" + path)
|
||||||
|
|
||||||
|
path = "/" + path
|
||||||
|
path_components = path.split("/")
|
||||||
|
|
||||||
|
if path_components[1] not in {"post", "user", "", "player"}:
|
||||||
|
# serve index.htm like normal
|
||||||
|
return serve_without_embed()
|
||||||
|
|
||||||
|
if path_components[1] == "player" and path_components[2]:
|
||||||
|
try:
|
||||||
|
post_id = int(path_components[2])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return serve_twitter_video_player(start_response, post_id)
|
||||||
|
|
||||||
|
server_info = requests.get(BACKEND_BASE_URL + "/api/info").json()
|
||||||
|
privileges = server_info["config"]["privileges"]
|
||||||
|
# Telegram prefers twitter:card to og:video, so we need to skip the former in order for videos to play inline
|
||||||
|
skip_twitter_player = env["HTTP_USER_AGENT"].startswith("TelegramBot")
|
||||||
|
if path_components[1] == "user":
|
||||||
|
username = path_components[2]
|
||||||
|
if privileges["users:view"] != "anonymous" or not username:
|
||||||
|
return serve_without_embed()
|
||||||
|
metadata = user_embed(username)
|
||||||
|
|
||||||
|
elif path_components[1] == "post":
|
||||||
|
try:
|
||||||
|
post_id = int(path_components[2])
|
||||||
|
except ValueError:
|
||||||
|
return serve_without_embed()
|
||||||
|
|
||||||
|
if privileges["posts:view"] != "anonymous":
|
||||||
|
return serve_without_embed()
|
||||||
|
metadata = post_embed(post_id, skip_twitter_player=skip_twitter_player)
|
||||||
|
|
||||||
|
elif path_components[1] == "":
|
||||||
|
metadata = homepage_embed(server_info, skip_twitter_player=skip_twitter_player)
|
||||||
|
|
||||||
|
metadata = itertools.chain(general_embed(server_info), metadata)
|
||||||
|
body = INDEX_HTML.replace("<!-- Embed Placeholder -->", render_embed(metadata)).encode("utf-8")
|
||||||
|
start_response(http_status.OK, [("Content-Type", "text/html"), ("Content-Length", str(len(body)))])
|
||||||
|
return (body,)
|
@ -789,7 +789,7 @@ data.
|
|||||||
| `fav-time` | alias of `fav-date` |
|
| `fav-time` | alias of `fav-date` |
|
||||||
| `feature-date` | featured at given date |
|
| `feature-date` | featured at given date |
|
||||||
| `feature-time` | alias of `feature-time` |
|
| `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` |
|
| `rating` | alias of `safety` |
|
||||||
|
|
||||||
**Sort style tokens**
|
**Sort style tokens**
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
This assumes that you have Docker (version 17.05 or greater)
|
This assumes that you have Docker (version 19.03 or greater)
|
||||||
and Docker Compose (version 1.6.0 or greater) already installed.
|
and the Docker Compose CLI (version 1.27.0 or greater) already installed.
|
||||||
|
|
||||||
### Prepare things
|
### 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:
|
This pulls the latest containers from docker.io:
|
||||||
```console
|
```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
|
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:
|
For first run, it is recommended to start the database separately:
|
||||||
```console
|
```console
|
||||||
user@host:szuru$ docker-compose up -d sql
|
user@host:szuru$ docker compose up -d sql
|
||||||
```
|
```
|
||||||
|
|
||||||
To start all containers:
|
To start all containers:
|
||||||
```console
|
```console
|
||||||
user@host:szuru$ docker-compose up -d
|
user@host:szuru$ docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
To view/monitor the application logs:
|
To view/monitor the application logs:
|
||||||
```console
|
```console
|
||||||
user@host:szuru$ docker-compose logs -f
|
user@host:szuru$ docker compose logs -f
|
||||||
# (CTRL+C to exit)
|
# (CTRL+C to exit)
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -84,13 +84,13 @@ and Docker Compose (version 1.6.0 or greater) already installed.
|
|||||||
2. Build the containers:
|
2. Build the containers:
|
||||||
|
|
||||||
```console
|
```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`
|
That will attempt to build both containers, but you can specify `client`
|
||||||
or `server` to make it build only one.
|
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
|
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
|
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
|
*Note: If your changes are not taking effect in your builds, consider building
|
||||||
with `--no-cache`.*
|
with `--no-cache`.*
|
||||||
@ -117,7 +117,7 @@ with `--no-cache`.*
|
|||||||
run from docker:
|
run from docker:
|
||||||
|
|
||||||
```console
|
```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.
|
will give you a breakdown on all available commands.
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
## Example Docker Compose configuration
|
## 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
|
## orchestration services
|
||||||
version: '2'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
server:
|
server:
|
||||||
|
@ -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 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."
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user