Vlastní stránky a odkazy + readme
Tento commit je obsažen v:
rodič
a96c86a722
revize
9b75c72ee8
3
.gitignore
vendorováno
3
.gitignore
vendorováno
@ -1,7 +1,6 @@
|
||||
/pages/*.html
|
||||
/target
|
||||
/templates_min
|
||||
/uploads
|
||||
Nekrochan.toml
|
||||
.env
|
||||
|
||||
cloud-run.sh
|
||||
|
47
Nekrochan.toml.template
Spustitelný soubor
47
Nekrochan.toml.template
Spustitelný soubor
@ -0,0 +1,47 @@
|
||||
[server]
|
||||
port = ${PORT}
|
||||
database_url = "postgres://${DB_HOST}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
|
||||
cache_url = "redis://${REDIS_HOST}/${REDIS_DB}"
|
||||
|
||||
[site]
|
||||
name = "${SITE_NAME}"
|
||||
description = "${SITE_DESCRIPTION}"
|
||||
theme = "yotsuba.css"
|
||||
links = []
|
||||
noko = true
|
||||
|
||||
[secrets]
|
||||
auth_token = "${AUTH_SECRET}"
|
||||
secure_trip = "${TRIP_SECRET}"
|
||||
user_id = "${UID_SECRET}"
|
||||
|
||||
[files]
|
||||
videos = true
|
||||
thumb_size = 150
|
||||
max_size_mb = 50
|
||||
max_height = 10000
|
||||
max_width = 10000
|
||||
cleanup_interval = 3600
|
||||
|
||||
[board_defaults]
|
||||
anon_name = "Anonym"
|
||||
page_size = 10
|
||||
page_count = 20
|
||||
file_limit = 1
|
||||
bump_limit = 500
|
||||
reply_limit = 1000
|
||||
locked = false
|
||||
user_ids = false
|
||||
flags = false
|
||||
thread_captcha = "off"
|
||||
reply_captcha = "off"
|
||||
board_theme = "yotsuba.css"
|
||||
require_thread_content = true
|
||||
require_thread_file = true
|
||||
require_reply_content = false
|
||||
require_reply_file = false
|
||||
antispam = true
|
||||
antispam_ip = 5
|
||||
antispam_content = 10
|
||||
antispam_both = 60
|
||||
thread_cooldown = 60
|
98
README.md
98
README.md
@ -2,6 +2,102 @@
|
||||
|
||||
100% český imidžbórdový skript
|
||||
|
||||
>100% český přestože je kód anglicky...
|
||||
> 100% český přestože je kód anglicky...
|
||||
|
||||
Brzy dostupný na https://czchan.org/.
|
||||
|
||||
## Tutoriál nebo něco
|
||||
|
||||
Pravděpodobně to běží jenom na Linuxu, ale nikdo na serverech Windows stejně nepoužívá. Tutoriál počítá se systémem Ubuntu a je možné, že je nekompletní.
|
||||
|
||||
### Nainstaluj Rust
|
||||
|
||||
Ne, nejsem transka (zatím).
|
||||
|
||||
```
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
```
|
||||
|
||||
### Nainstaluj ostatní požadavky
|
||||
|
||||
```
|
||||
# Potřebné ke kompilaci
|
||||
sudo apt install binutils build-essential libssl-dev libpq-dev postgresql
|
||||
# Potřebné k funkci
|
||||
sudo apt install imagemagick ffmpeg
|
||||
```
|
||||
|
||||
### Vytvoř databázi
|
||||
|
||||
```
|
||||
sudo adduser nekrochan --system --disabled-login --no-create-home --group
|
||||
sudo passwd nekrochan # Nastavíme heslo pro systémového uživatele
|
||||
sudo -iu postgres psql -c "CREATE USER nekrochan WITH PASSWORD 'password';"
|
||||
sudo -iu postgres psql -c "CREATE DATABASE nekrochan WITH OWNER nekrochan;"
|
||||
```
|
||||
|
||||
### Automatická konfigurace
|
||||
|
||||
```
|
||||
chmod +x ./configure.sh
|
||||
./configure.sh
|
||||
```
|
||||
|
||||
### Nastartuj server
|
||||
|
||||
```
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
### Vytvoř nástěnku
|
||||
|
||||
Po kompilaci by se měl spustit server na https://localhost:7000/. Stránka ti pravděpodobně řekne, že ještě nebyla inicializována domovní stránka. Je potřeba vytvořit nástěnku. Nejdříve je ale potřeba vytvořit administátorský účet.
|
||||
|
||||
Heslo v příkladu je "password", můžeš použít příklad a heslo změnit potom v administrátorském rozhraní.
|
||||
|
||||
```
|
||||
sudo -iu nekrochan psql -c "INSERT INTO accounts (username, password, owner, permissions) VALUES ('admin', '$2y$10$jHl27pbYNvgpQxksmF.N/O0IHrfFBDY1Tg/qBX/UwrMa3j7owkiQm', true, '131072'::jsonb);"
|
||||
```
|
||||
|
||||
Po příkazu budeš muset restartovat server, aby se změna projevila v mezipaměti.
|
||||
|
||||
Nástěnka lze vytvořit po přihlášení na https://localhost:7000/login na stránce https://localhost:7000/staff/boards
|
||||
|
||||
### Automatický start
|
||||
|
||||
Nejprve vytvoříme složku pro nekrochan a zkopírujeme tam potřebné soubory.
|
||||
|
||||
```
|
||||
sudo mkdir -p /srv/nekrochan
|
||||
sudo chown nekrochan:nekrochan /srv/nekrochan
|
||||
sudo cp -r ./target/release/nekrochan Nekrochan.toml ./pages ./static ./uploads /srv/nekrochan/
|
||||
```
|
||||
|
||||
Nyní vytvoříme skript pro systemd, aby server automaticky nastartovat po zapnutí počítače. Uložíme ho jako `/etc/systemd/system/nekrochan.service`.
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Nekrochan
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=nekrochan
|
||||
ExecStart=/srv/nekrochan/nekrochan
|
||||
WorkingDirectory=/srv/nekrochan
|
||||
Environment=RUST_LOG="info"
|
||||
Restart=on-failure
|
||||
|
||||
ProtectSystem=yes
|
||||
PrivateTmp=true
|
||||
MemoryDenyWriteExecute=true
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### Další konfigurace
|
||||
|
||||
Většinu možností najdeš v souboru `Nekrochan.toml`. Vlastní stránky (např. pravidla, faq apod.) můžeš nahrávat do složky `pages`.
|
||||
|
||||
Také budeš pravděpodobně chtít nastavit reverzní proxy, např. NGINX. IP adresu posílej serveru v hlavičce `X-Forwarded-For` a kód země (potřebný pro vlajky) v hlavičce `X-Country-Code`.
|
||||
|
68
configure.sh
Spustitelný soubor
68
configure.sh
Spustitelný soubor
@ -0,0 +1,68 @@
|
||||
set -e
|
||||
|
||||
echo "# Výběr portu"
|
||||
read -p "Port serveru [7000]: " port
|
||||
|
||||
echo "# Konfigurace databáze"
|
||||
read -p "Host databáze [localhost]: " db_host
|
||||
read -p "Port databáze [5432]: " db_port
|
||||
|
||||
read -p "Uživatelské jméno: " db_user
|
||||
if [ -z ${db_user} ]
|
||||
then
|
||||
echo "Uživatelské jméno je povinné"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
read -p "Heslo: " db_password
|
||||
if [ -z ${db_password} ]
|
||||
then
|
||||
echo "Heslo je povinné"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
read -p "Jméno databáze: " db_name
|
||||
if [ -z ${db_name} ]
|
||||
then
|
||||
echo "Jméno databáze je povinné"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "# Konfigurace redisu"
|
||||
read -p "Host redisu [localhost]: " redis_host
|
||||
read -p "Číslo databáze [0]: " redis_db
|
||||
|
||||
echo "# Konfigurace stránky"
|
||||
|
||||
read -p "Jméno stránky: " site_name
|
||||
if [ -z ${site_name} ]
|
||||
then
|
||||
echo "Jméno stránky je povinné"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
read -p "Popis stránky: " site_description
|
||||
if [ -z ${site_description} ]
|
||||
then
|
||||
echo "Popis stránky je povinný"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PORT=${port:-7000}
|
||||
export DB_HOST=${db_host:-localhost}
|
||||
export DB_PORT=${db_host:-5432}
|
||||
export DB_USER=${db_user}
|
||||
export DB_PASSWORD=${db_password}
|
||||
export DB_NAME=${db_name}
|
||||
export REDIS_HOST=${redis_host:-localhost}
|
||||
export REDIS_DB=${redis_db:-0}
|
||||
export REDIS_HOST=${redis_host:-localhost}
|
||||
export SITE_NAME=${site_name}
|
||||
export SITE_DESCRIPTION=${site_description}
|
||||
|
||||
export AUTH_SECRET=`tr -dc A-Za-z0-9 </dev/urandom | head -c 16`
|
||||
export TRIP_SECRET=`tr -dc A-Za-z0-9 </dev/urandom | head -c 16`
|
||||
export UID_SECRET=`tr -dc A-Za-z0-9 </dev/urandom | head -c 16`
|
||||
|
||||
envsubst < Nekrochan.toml.template > Nekrochan.toml
|
||||
mkdir -p ./uploads/thumb
|
@ -26,5 +26,3 @@ CREATE TABLE bans (
|
||||
expires TIMESTAMPTZ DEFAULT NULL,
|
||||
created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO accounts (username, password, owner, permissions) VALUES ('admin', '$2y$10$XcxAe19B1eWC15sfnDRyiuiNLZIhdL7PMTnTmtTfglJIz0zOpN3oa', true, '16383'::jsonb);
|
||||
|
@ -31,7 +31,9 @@ pub struct ServerCfg {
|
||||
pub struct SiteCfg {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub default_noko: bool,
|
||||
pub theme: String,
|
||||
pub links: Vec<Vec<(String, String)>>,
|
||||
pub noko: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@ -73,4 +75,5 @@ pub struct BoardCfg {
|
||||
pub antispam_ip: i64,
|
||||
pub antispam_content: i64,
|
||||
pub antispam_both: i64,
|
||||
pub thread_cooldown: i64,
|
||||
}
|
||||
|
@ -68,13 +68,31 @@ pub async fn init_cache(ctx: &Ctx) -> Result<(), Error> {
|
||||
|
||||
for post in posts {
|
||||
let ip_key = format!("by_ip:{}", post.ip);
|
||||
let content_key = format!("by_content:{}", digest(post.content_nomarkup));
|
||||
let content_key = format!(
|
||||
"by_content:{}",
|
||||
digest(post.content_nomarkup.to_lowercase())
|
||||
);
|
||||
|
||||
let member = format!("{}/{}", post.board, post.id);
|
||||
let score = post.created.timestamp_micros();
|
||||
|
||||
ctx.cache().zadd(ip_key, &member, score).await?;
|
||||
ctx.cache().zadd(content_key, &member, score).await?;
|
||||
|
||||
if post.thread.is_none() {
|
||||
let key = format!("last_thread:{}", post.ip);
|
||||
let last_thread = ctx
|
||||
.cache()
|
||||
.get::<_, Option<i64>>(&key)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let timestamp = post.created.timestamp_micros();
|
||||
|
||||
if timestamp > last_thread {
|
||||
ctx.cache().set(key, timestamp).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,13 +78,23 @@ impl Post {
|
||||
}
|
||||
|
||||
let ip_key = format!("by_ip:{ip}");
|
||||
let content_key = format!("by_content:{}", digest(post.content_nomarkup.as_bytes()));
|
||||
let content_key = format!(
|
||||
"by_content:{}",
|
||||
digest(post.content_nomarkup.to_lowercase())
|
||||
);
|
||||
|
||||
let member = format!("{}/{}", board.id, post.id);
|
||||
let score = post.created.timestamp_micros();
|
||||
|
||||
ctx.cache().zadd(ip_key, &member, score).await?;
|
||||
ctx.cache().zadd(content_key, &member, score).await?;
|
||||
|
||||
if thread.is_none() {
|
||||
ctx.cache()
|
||||
.set(format!("last_thread:{ip}"), post.created.timestamp_micros())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(post)
|
||||
}
|
||||
|
||||
@ -116,13 +126,10 @@ impl Post {
|
||||
}
|
||||
|
||||
pub async fn read(ctx: &Ctx, board: String, id: i64) -> Result<Option<Self>, NekrochanError> {
|
||||
let post = query_as(&format!(
|
||||
"SELECT * FROM posts_{} WHERE id = $1",
|
||||
board
|
||||
))
|
||||
.bind(id)
|
||||
.fetch_optional(ctx.db())
|
||||
.await?;
|
||||
let post = query_as(&format!("SELECT * FROM posts_{} WHERE id = $1", board))
|
||||
.bind(id)
|
||||
.fetch_optional(ctx.db())
|
||||
.await?;
|
||||
|
||||
Ok(post)
|
||||
}
|
||||
@ -329,8 +336,11 @@ impl Post {
|
||||
.fetch_one(ctx.db())
|
||||
.await?;
|
||||
|
||||
let old_key = format!("by_content:{}", digest(self.content_nomarkup.as_bytes()));
|
||||
let new_key = format!("by_content:{}", digest(content_nomarkup.as_bytes()));
|
||||
let old_key = format!(
|
||||
"by_content:{}",
|
||||
digest(self.content_nomarkup.to_lowercase())
|
||||
);
|
||||
let new_key = format!("by_content:{}", digest(content_nomarkup.to_lowercase()));
|
||||
let member = format!("{}/{}", self.board, self.id);
|
||||
let score = Utc::now().timestamp_micros();
|
||||
|
||||
@ -423,7 +433,10 @@ impl Post {
|
||||
}
|
||||
|
||||
let ip_key = format!("by_ip:{}", post.ip);
|
||||
let content_key = format!("by_content:{}", digest(post.content_nomarkup.as_bytes()));
|
||||
let content_key = format!(
|
||||
"by_content:{}",
|
||||
digest(post.content_nomarkup.to_lowercase())
|
||||
);
|
||||
|
||||
let member = format!("{}/{}", post.board, post.id);
|
||||
|
||||
|
@ -76,6 +76,8 @@ pub enum NekrochanError {
|
||||
OverboardError,
|
||||
#[error("Účet vlastníka nemůže být vymazán.")]
|
||||
OwnerDeletionError,
|
||||
#[error("Stránka {} neexistuje", .0)]
|
||||
PageNotFound(String),
|
||||
#[error("Heslo musí mít alespoň 8 znaků.")]
|
||||
PasswordFormatError,
|
||||
#[error("Jméno nesmí mít více než 32 znaků.")]
|
||||
@ -249,6 +251,7 @@ impl ResponseError for NekrochanError {
|
||||
NekrochanError::NotLoggedInError => StatusCode::UNAUTHORIZED,
|
||||
NekrochanError::OverboardError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
NekrochanError::OwnerDeletionError => StatusCode::FORBIDDEN,
|
||||
NekrochanError::PageNotFound(_) => StatusCode::NOT_FOUND,
|
||||
NekrochanError::PasswordFormatError => StatusCode::BAD_REQUEST,
|
||||
NekrochanError::PostNameFormatError => StatusCode::BAD_REQUEST,
|
||||
NekrochanError::PostNotFound(_, _) => StatusCode::NOT_FOUND,
|
||||
|
@ -3,7 +3,7 @@ use db::models::{Board, Post};
|
||||
use error::NekrochanError;
|
||||
use web::tcx::TemplateCtx;
|
||||
|
||||
const GENERIC_PAGE_SIZE: i64 = 15;
|
||||
const GENERIC_PAGE_SIZE: i64 = 10;
|
||||
|
||||
pub mod auth;
|
||||
pub mod cfg;
|
||||
|
@ -73,6 +73,7 @@ async fn run() -> Result<(), Error> {
|
||||
.service(web::news::news)
|
||||
.service(web::overboard::overboard)
|
||||
.service(web::overboard_catalog::overboard_catalog)
|
||||
.service(web::page::page)
|
||||
.service(web::thread_json::thread_json)
|
||||
.service(web::thread::thread)
|
||||
.service(web::actions::appeal_ban::appeal_ban)
|
||||
|
@ -65,7 +65,7 @@ pub async fn create_post(
|
||||
}
|
||||
|
||||
let mut bump = true;
|
||||
let mut noko = ctx.cfg.site.default_noko;
|
||||
let mut noko = ctx.cfg.site.noko;
|
||||
|
||||
let thread = match form.thread {
|
||||
Some(Text(thread)) => {
|
||||
@ -141,11 +141,11 @@ pub async fn create_post(
|
||||
bump = false;
|
||||
}
|
||||
|
||||
if !ctx.cfg.site.default_noko && email_lower == "noko" {
|
||||
if !ctx.cfg.site.noko && email_lower == "noko" {
|
||||
noko = true
|
||||
}
|
||||
|
||||
if ctx.cfg.site.default_noko {
|
||||
if ctx.cfg.site.noko {
|
||||
if email_lower == "nonoko" {
|
||||
noko = false;
|
||||
}
|
||||
@ -322,5 +322,14 @@ pub async fn check_spam(
|
||||
return Err(NekrochanError::FloodError);
|
||||
}
|
||||
|
||||
let last_thread: i64 = ctx.cache().get(format!("last_thread:{ip}")).await?;
|
||||
|
||||
let since_last_thread = Utc::now().timestamp_micros() - last_thread;
|
||||
let since_last_thread = Duration::microseconds(since_last_thread);
|
||||
|
||||
if since_last_thread.num_seconds() < board.config.thread_cooldown {
|
||||
return Err(NekrochanError::FloodError);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -11,10 +11,11 @@ pub mod logout;
|
||||
pub mod news;
|
||||
pub mod overboard;
|
||||
pub mod overboard_catalog;
|
||||
pub mod thread_json;
|
||||
pub mod page;
|
||||
pub mod staff;
|
||||
pub mod tcx;
|
||||
pub mod thread;
|
||||
pub mod thread_json;
|
||||
|
||||
use actix_web::{http::StatusCode, HttpRequest, HttpResponse, HttpResponseBuilder};
|
||||
use askama::Template;
|
||||
|
36
src/web/page.rs
Normální soubor
36
src/web/page.rs
Normální soubor
@ -0,0 +1,36 @@
|
||||
use actix_web::{
|
||||
get,
|
||||
web::{Data, Path},
|
||||
HttpRequest, HttpResponse,
|
||||
};
|
||||
use askama::Template;
|
||||
use tokio::fs::read_to_string;
|
||||
|
||||
use crate::{ctx::Ctx, error::NekrochanError, web::template_response};
|
||||
|
||||
use super::tcx::TemplateCtx;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "page.html")]
|
||||
struct PageTemplate {
|
||||
pub tcx: TemplateCtx,
|
||||
pub name: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[get("/page/{name}")]
|
||||
pub async fn page(
|
||||
ctx: Data<Ctx>,
|
||||
req: HttpRequest,
|
||||
name: Path<String>,
|
||||
) -> Result<HttpResponse, NekrochanError> {
|
||||
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
||||
let name = name.into_inner();
|
||||
let content = read_to_string(format!("./pages/{name}.html"))
|
||||
.await
|
||||
.map_err(|_| NekrochanError::PageNotFound(name.clone()))?;
|
||||
|
||||
let template = PageTemplate { tcx, name, content };
|
||||
|
||||
template_response(&template)
|
||||
}
|
@ -8,7 +8,7 @@ use crate::{
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref ID_REGEX: Regex = Regex::new(r#"^\w{1,16}$"#).unwrap();
|
||||
static ref ID_REGEX: Regex = Regex::new(r"^\w{1,16}$").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -29,6 +29,7 @@ pub struct UpdateBoardConfigForm {
|
||||
antispam_ip: i64,
|
||||
antispam_content: i64,
|
||||
antispam_both: i64,
|
||||
thread_cooldown: i64,
|
||||
}
|
||||
|
||||
#[post("/staff/actions/update-board-config")]
|
||||
@ -68,6 +69,7 @@ pub async fn update_board_config(
|
||||
let antispam_ip = form.antispam_ip;
|
||||
let antispam_content = form.antispam_content;
|
||||
let antispam_both = form.antispam_both;
|
||||
let thread_cooldown = form.thread_cooldown;
|
||||
|
||||
let config = BoardCfg {
|
||||
anon_name,
|
||||
@ -90,6 +92,7 @@ pub async fn update_board_config(
|
||||
antispam_ip,
|
||||
antispam_content,
|
||||
antispam_both,
|
||||
thread_cooldown,
|
||||
};
|
||||
|
||||
board.update_config(&ctx, config).await?;
|
||||
|
@ -39,7 +39,7 @@ pub async fn thread_json(
|
||||
let mut res = HashMap::new();
|
||||
|
||||
let replies = thread.read_replies(&ctx).await?;
|
||||
let posts = vec![vec![thread], replies].concat();
|
||||
let posts = [vec![thread], replies].concat();
|
||||
|
||||
for post in posts {
|
||||
let id = post.id;
|
||||
|
@ -7,7 +7,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta name="description" content="{{ tcx.cfg.site.description }}">
|
||||
<link rel="stylesheet" href='/static/themes/{% block theme %}{% include "../theme.txt" %}{% endblock %}'>
|
||||
<link rel="stylesheet" href="/static/themes/{% block theme %}{{ tcx.cfg.site.theme }}{% endblock %}">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/js/jquery.min.js"></script>
|
||||
<!-- UX scripts -->
|
||||
@ -26,8 +26,18 @@
|
||||
<span class="link-group">
|
||||
<a href="/overboard">nadnástěnka</a>
|
||||
<span class="link-separator"></span>
|
||||
<a href="/news">novinky</a></span>
|
||||
<a href="/news">novinky</a>
|
||||
</span>
|
||||
{% for group in tcx.cfg.site.links %}
|
||||
<span class="link-group">
|
||||
{% for (link, href) in group %}
|
||||
<a href="{{ href }}">{{ link }}</a>
|
||||
{% if !loop.last %}
|
||||
<span class="link-separator"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endfor %}
|
||||
<span class="float-r">
|
||||
{% if tcx.account.is_some() %}
|
||||
<span class="link-group">
|
||||
|
@ -4,7 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chyba</title>
|
||||
<link rel="stylesheet" href='/static/themes/{% include "../theme.txt" %}'>
|
||||
{# Error pages are yotsuba (they just are, okay?) #}
|
||||
{# Go ahead and edit this manually if you actually care #}
|
||||
<link rel="stylesheet" href="/static/themes/yotsuba.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
3
templates/page.html
Normální soubor
3
templates/page.html
Normální soubor
@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ tcx.cfg.site.name }} ({{ name }}){% endblock %}
|
||||
{% block content %}{{ content|safe }}{% endblock %}
|
@ -174,6 +174,11 @@
|
||||
<td><input name="antispam_both" type="number" min="0" value="{{ board.config.0.antispam_both }}" required=""></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="label">Interval mezi vlákny (IP)</td>
|
||||
<td><input name="thread_cooldown" type="number" min="0" value="{{ board.config.0.antispam_both }}" required=""></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2"><input class="button" type="submit" value="Uložit"></td>
|
||||
</tr>
|
||||
|
@ -1 +0,0 @@
|
||||
yotsuba.css
|
Načítá se…
Odkázat v novém úkolu
Zablokovat Uživatele