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
|
/target
|
||||||
/templates_min
|
/templates_min
|
||||||
/uploads
|
/uploads
|
||||||
Nekrochan.toml
|
Nekrochan.toml
|
||||||
.env
|
.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ý 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/.
|
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,
|
expires TIMESTAMPTZ DEFAULT NULL,
|
||||||
created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
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 struct SiteCfg {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub default_noko: bool,
|
pub theme: String,
|
||||||
|
pub links: Vec<Vec<(String, String)>>,
|
||||||
|
pub noko: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
@ -73,4 +75,5 @@ pub struct BoardCfg {
|
|||||||
pub antispam_ip: i64,
|
pub antispam_ip: i64,
|
||||||
pub antispam_content: i64,
|
pub antispam_content: i64,
|
||||||
pub antispam_both: 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 {
|
for post in posts {
|
||||||
let ip_key = format!("by_ip:{}", post.ip);
|
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 member = format!("{}/{}", post.board, post.id);
|
||||||
let score = post.created.timestamp_micros();
|
let score = post.created.timestamp_micros();
|
||||||
|
|
||||||
ctx.cache().zadd(ip_key, &member, score).await?;
|
ctx.cache().zadd(ip_key, &member, score).await?;
|
||||||
ctx.cache().zadd(content_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 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 member = format!("{}/{}", board.id, post.id);
|
||||||
let score = post.created.timestamp_micros();
|
let score = post.created.timestamp_micros();
|
||||||
|
|
||||||
ctx.cache().zadd(ip_key, &member, score).await?;
|
ctx.cache().zadd(ip_key, &member, score).await?;
|
||||||
ctx.cache().zadd(content_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)
|
Ok(post)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,13 +126,10 @@ impl Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn read(ctx: &Ctx, board: String, id: i64) -> Result<Option<Self>, NekrochanError> {
|
pub async fn read(ctx: &Ctx, board: String, id: i64) -> Result<Option<Self>, NekrochanError> {
|
||||||
let post = query_as(&format!(
|
let post = query_as(&format!("SELECT * FROM posts_{} WHERE id = $1", board))
|
||||||
"SELECT * FROM posts_{} WHERE id = $1",
|
.bind(id)
|
||||||
board
|
.fetch_optional(ctx.db())
|
||||||
))
|
.await?;
|
||||||
.bind(id)
|
|
||||||
.fetch_optional(ctx.db())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(post)
|
Ok(post)
|
||||||
}
|
}
|
||||||
@ -329,8 +336,11 @@ impl Post {
|
|||||||
.fetch_one(ctx.db())
|
.fetch_one(ctx.db())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let old_key = format!("by_content:{}", digest(self.content_nomarkup.as_bytes()));
|
let old_key = format!(
|
||||||
let new_key = format!("by_content:{}", digest(content_nomarkup.as_bytes()));
|
"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 member = format!("{}/{}", self.board, self.id);
|
||||||
let score = Utc::now().timestamp_micros();
|
let score = Utc::now().timestamp_micros();
|
||||||
|
|
||||||
@ -423,7 +433,10 @@ impl Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let ip_key = format!("by_ip:{}", post.ip);
|
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);
|
let member = format!("{}/{}", post.board, post.id);
|
||||||
|
|
||||||
|
@ -76,6 +76,8 @@ pub enum NekrochanError {
|
|||||||
OverboardError,
|
OverboardError,
|
||||||
#[error("Účet vlastníka nemůže být vymazán.")]
|
#[error("Účet vlastníka nemůže být vymazán.")]
|
||||||
OwnerDeletionError,
|
OwnerDeletionError,
|
||||||
|
#[error("Stránka {} neexistuje", .0)]
|
||||||
|
PageNotFound(String),
|
||||||
#[error("Heslo musí mít alespoň 8 znaků.")]
|
#[error("Heslo musí mít alespoň 8 znaků.")]
|
||||||
PasswordFormatError,
|
PasswordFormatError,
|
||||||
#[error("Jméno nesmí mít více než 32 znaků.")]
|
#[error("Jméno nesmí mít více než 32 znaků.")]
|
||||||
@ -249,6 +251,7 @@ impl ResponseError for NekrochanError {
|
|||||||
NekrochanError::NotLoggedInError => StatusCode::UNAUTHORIZED,
|
NekrochanError::NotLoggedInError => StatusCode::UNAUTHORIZED,
|
||||||
NekrochanError::OverboardError => StatusCode::INTERNAL_SERVER_ERROR,
|
NekrochanError::OverboardError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
NekrochanError::OwnerDeletionError => StatusCode::FORBIDDEN,
|
NekrochanError::OwnerDeletionError => StatusCode::FORBIDDEN,
|
||||||
|
NekrochanError::PageNotFound(_) => StatusCode::NOT_FOUND,
|
||||||
NekrochanError::PasswordFormatError => StatusCode::BAD_REQUEST,
|
NekrochanError::PasswordFormatError => StatusCode::BAD_REQUEST,
|
||||||
NekrochanError::PostNameFormatError => StatusCode::BAD_REQUEST,
|
NekrochanError::PostNameFormatError => StatusCode::BAD_REQUEST,
|
||||||
NekrochanError::PostNotFound(_, _) => StatusCode::NOT_FOUND,
|
NekrochanError::PostNotFound(_, _) => StatusCode::NOT_FOUND,
|
||||||
|
@ -3,7 +3,7 @@ use db::models::{Board, Post};
|
|||||||
use error::NekrochanError;
|
use error::NekrochanError;
|
||||||
use web::tcx::TemplateCtx;
|
use web::tcx::TemplateCtx;
|
||||||
|
|
||||||
const GENERIC_PAGE_SIZE: i64 = 15;
|
const GENERIC_PAGE_SIZE: i64 = 10;
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod cfg;
|
pub mod cfg;
|
||||||
|
@ -73,6 +73,7 @@ async fn run() -> Result<(), Error> {
|
|||||||
.service(web::news::news)
|
.service(web::news::news)
|
||||||
.service(web::overboard::overboard)
|
.service(web::overboard::overboard)
|
||||||
.service(web::overboard_catalog::overboard_catalog)
|
.service(web::overboard_catalog::overboard_catalog)
|
||||||
|
.service(web::page::page)
|
||||||
.service(web::thread_json::thread_json)
|
.service(web::thread_json::thread_json)
|
||||||
.service(web::thread::thread)
|
.service(web::thread::thread)
|
||||||
.service(web::actions::appeal_ban::appeal_ban)
|
.service(web::actions::appeal_ban::appeal_ban)
|
||||||
|
@ -65,7 +65,7 @@ pub async fn create_post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut bump = true;
|
let mut bump = true;
|
||||||
let mut noko = ctx.cfg.site.default_noko;
|
let mut noko = ctx.cfg.site.noko;
|
||||||
|
|
||||||
let thread = match form.thread {
|
let thread = match form.thread {
|
||||||
Some(Text(thread)) => {
|
Some(Text(thread)) => {
|
||||||
@ -141,11 +141,11 @@ pub async fn create_post(
|
|||||||
bump = false;
|
bump = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.cfg.site.default_noko && email_lower == "noko" {
|
if !ctx.cfg.site.noko && email_lower == "noko" {
|
||||||
noko = true
|
noko = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.cfg.site.default_noko {
|
if ctx.cfg.site.noko {
|
||||||
if email_lower == "nonoko" {
|
if email_lower == "nonoko" {
|
||||||
noko = false;
|
noko = false;
|
||||||
}
|
}
|
||||||
@ -322,5 +322,14 @@ pub async fn check_spam(
|
|||||||
return Err(NekrochanError::FloodError);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,11 @@ pub mod logout;
|
|||||||
pub mod news;
|
pub mod news;
|
||||||
pub mod overboard;
|
pub mod overboard;
|
||||||
pub mod overboard_catalog;
|
pub mod overboard_catalog;
|
||||||
pub mod thread_json;
|
pub mod page;
|
||||||
pub mod staff;
|
pub mod staff;
|
||||||
pub mod tcx;
|
pub mod tcx;
|
||||||
pub mod thread;
|
pub mod thread;
|
||||||
|
pub mod thread_json;
|
||||||
|
|
||||||
use actix_web::{http::StatusCode, HttpRequest, HttpResponse, HttpResponseBuilder};
|
use actix_web::{http::StatusCode, HttpRequest, HttpResponse, HttpResponseBuilder};
|
||||||
use askama::Template;
|
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! {
|
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)]
|
#[derive(Deserialize)]
|
||||||
|
@ -57,7 +57,7 @@ pub async fn edit_news(
|
|||||||
newspost
|
newspost
|
||||||
.update(&ctx, content, content_nomarkup.into())
|
.update(&ctx, content, content_nomarkup.into())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
news_edited += 1;
|
news_edited += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ pub struct UpdateBoardConfigForm {
|
|||||||
antispam_ip: i64,
|
antispam_ip: i64,
|
||||||
antispam_content: i64,
|
antispam_content: i64,
|
||||||
antispam_both: i64,
|
antispam_both: i64,
|
||||||
|
thread_cooldown: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/staff/actions/update-board-config")]
|
#[post("/staff/actions/update-board-config")]
|
||||||
@ -68,6 +69,7 @@ pub async fn update_board_config(
|
|||||||
let antispam_ip = form.antispam_ip;
|
let antispam_ip = form.antispam_ip;
|
||||||
let antispam_content = form.antispam_content;
|
let antispam_content = form.antispam_content;
|
||||||
let antispam_both = form.antispam_both;
|
let antispam_both = form.antispam_both;
|
||||||
|
let thread_cooldown = form.thread_cooldown;
|
||||||
|
|
||||||
let config = BoardCfg {
|
let config = BoardCfg {
|
||||||
anon_name,
|
anon_name,
|
||||||
@ -90,6 +92,7 @@ pub async fn update_board_config(
|
|||||||
antispam_ip,
|
antispam_ip,
|
||||||
antispam_content,
|
antispam_content,
|
||||||
antispam_both,
|
antispam_both,
|
||||||
|
thread_cooldown,
|
||||||
};
|
};
|
||||||
|
|
||||||
board.update_config(&ctx, config).await?;
|
board.update_config(&ctx, config).await?;
|
||||||
|
@ -39,7 +39,7 @@ pub async fn thread_json(
|
|||||||
let mut res = HashMap::new();
|
let mut res = HashMap::new();
|
||||||
|
|
||||||
let replies = thread.read_replies(&ctx).await?;
|
let replies = thread.read_replies(&ctx).await?;
|
||||||
let posts = vec![vec![thread], replies].concat();
|
let posts = [vec![thread], replies].concat();
|
||||||
|
|
||||||
for post in posts {
|
for post in posts {
|
||||||
let id = post.id;
|
let id = post.id;
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
<meta name="description" content="{{ tcx.cfg.site.description }}">
|
<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">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
<script src="/static/js/jquery.min.js"></script>
|
<script src="/static/js/jquery.min.js"></script>
|
||||||
<!-- UX scripts -->
|
<!-- UX scripts -->
|
||||||
@ -26,8 +26,18 @@
|
|||||||
<span class="link-group">
|
<span class="link-group">
|
||||||
<a href="/overboard">nadnástěnka</a>
|
<a href="/overboard">nadnástěnka</a>
|
||||||
<span class="link-separator"></span>
|
<span class="link-separator"></span>
|
||||||
<a href="/news">novinky</a></span>
|
<a href="/news">novinky</a>
|
||||||
</span>
|
</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">
|
<span class="float-r">
|
||||||
{% if tcx.account.is_some() %}
|
{% if tcx.account.is_some() %}
|
||||||
<span class="link-group">
|
<span class="link-group">
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
<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">
|
||||||
<title>Chyba</title>
|
<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">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
<td><input name="antispam_both" type="number" min="0" value="{{ board.config.0.antispam_both }}" required=""></td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<td colspan="2"><input class="button" type="submit" value="Uložit"></td>
|
<td colspan="2"><input class="button" type="submit" value="Uložit"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -1 +0,0 @@
|
|||||||
yotsuba.css
|
|
Načítá se…
Odkázat v novém úkolu
Zablokovat Uživatele