Vlastní stránky a odkazy + readme

Tento commit je obsažen v:
sneedmaster 2024-03-03 21:02:17 +01:00
rodič a96c86a722
revize 9b75c72ee8
23 změnil soubory, kde provedl 344 přidání a 30 odebrání

3
.gitignore vendorováno
Zobrazit soubor

@ -1,7 +1,6 @@
/pages/*.html
/target
/templates_min
/uploads
Nekrochan.toml
.env
cloud-run.sh

47
Nekrochan.toml.template Spustitelný soubor
Zobrazit 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

Zobrazit soubor

@ -5,3 +5,99 @@
> 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
Zobrazit 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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

@ -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,10 +126,7 @@ 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
))
let post = query_as(&format!("SELECT * FROM posts_{} WHERE id = $1", board))
.bind(id)
.fetch_optional(ctx.db())
.await?;
@ -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);

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

@ -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(())
}

Zobrazit soubor

@ -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
Zobrazit 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)
}

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

@ -0,0 +1,3 @@
{% extends "base.html" %}
{% block title %}{{ tcx.cfg.site.name }} ({{ name }}){% endblock %}
{% block content %}{{ content|safe }}{% endblock %}

Zobrazit soubor

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

Zobrazit soubor

@ -1 +0,0 @@
yotsuba.css