diff --git a/README.md b/README.md index a1f2b38..6431abe 100644 --- a/README.md +++ b/README.md @@ -3,101 +3,3 @@ 100% český imidžbórdový skript > 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 postgres psql -d nekrochan -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`. diff --git a/build.rs b/build.rs index 49224eb..0dcc6d4 100755 --- a/build.rs +++ b/build.rs @@ -30,7 +30,7 @@ fn main() -> Result<(), Error> { } let html = read_to_string(&path)?; - let minified = minify(html)?.replace('\n', ""); + let minified = minify(html)?.replace('\n', "").replace(" ", " "); File::create(path)?.write_all(minified.as_bytes())?; } diff --git a/src/db/local_stats.rs b/src/db/local_stats.rs index 1f04679..136ee99 100644 --- a/src/db/local_stats.rs +++ b/src/db/local_stats.rs @@ -6,13 +6,13 @@ use crate::{ctx::Ctx, error::NekrochanError}; impl LocalStats { pub async fn read(ctx: &Ctx) -> Result { let (post_count,) = query_as( - "SELECT coalesce(sum(last_value)::bigint, 0) FROM pg_sequences WHERE sequencename LIKE 'posts_%_id_seq'", + "SELECT COALESCE(SUM(last_value)::bigint, 0) FROM pg_sequences WHERE sequencename LIKE 'posts_%_id_seq'", ) .fetch_one(ctx.db()) .await?; let (file_count, file_size) = query_as( - r#"SELECT count(files), coalesce(sum((files->>'size')::bigint)::bigint, 0) FROM ( + r#"SELECT COUNT(files), COALESCE(SUM((files->>'size')::bigint)::bigint, 0) FROM ( SELECT jsonb_array_elements(files) AS files FROM overboard ) flatten"#, ) diff --git a/src/db/post.rs b/src/db/post.rs index 3116d2a..8dfd198 100755 --- a/src/db/post.rs +++ b/src/db/post.rs @@ -277,6 +277,41 @@ impl Post { Ok(posts) } + pub async fn read_by_query( + ctx: &Ctx, + board: &Board, + query: String, + page: i64, + ) -> Result, NekrochanError> { + let posts = query_as(&format!( + "SELECT * FROM posts_{} WHERE LOWER(content_nomarkup) LIKE LOWER($1) ORDER BY created DESC LIMIT $2 OFFSET $3", + board.id + )) + .bind(format!("%{query}%")) + .bind(board.config.0.page_size) + .bind((page - 1) * board.config.0.page_size) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_by_query_overboard( + ctx: &Ctx, + query: String, + page: i64, + ) -> Result, NekrochanError> { + let posts = + query_as("SELECT * FROM overboard WHERE LOWER(content_nomarkup) LIKE LOWER($1) ORDER BY created DESC LIMIT $2 OFFSET $3") + .bind(format!("%{query}%")) + .bind(GENERIC_PAGE_SIZE) + .bind((page - 1) * GENERIC_PAGE_SIZE) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + pub async fn update_user_id(&self, ctx: &Ctx, user_id: String) -> Result<(), NekrochanError> { let post = query_as(&format!( "UPDATE posts_{} SET user_id = $1 WHERE id = $2 RETURNING *", diff --git a/src/error.rs b/src/error.rs index 6ec536d..97f4b5e 100755 --- a/src/error.rs +++ b/src/error.rs @@ -84,6 +84,8 @@ pub enum NekrochanError { PostNameFormatError, #[error("Příspěvek /{}/{} neexistuje.", .0, .1)] PostNotFound(String, i64), + #[error("Hledaný termín musí mít 1-256 znaků.")] + QueryFormatError, #[error("Vlákno dosáhlo limitu odpovědí.")] ReplyLimitError, #[error("Hlášení můsí mít 1-200 znaků.")] @@ -255,6 +257,7 @@ impl ResponseError for NekrochanError { NekrochanError::PasswordFormatError => StatusCode::BAD_REQUEST, NekrochanError::PostNameFormatError => StatusCode::BAD_REQUEST, NekrochanError::PostNotFound(_, _) => StatusCode::NOT_FOUND, + NekrochanError::QueryFormatError => StatusCode::BAD_REQUEST, NekrochanError::ReplyLimitError => StatusCode::FORBIDDEN, NekrochanError::ReportFormatError => StatusCode::BAD_REQUEST, NekrochanError::RequiredCaptchaError => StatusCode::UNAUTHORIZED, diff --git a/src/main.rs b/src/main.rs index 5bba90a..0d03e90 100755 --- a/src/main.rs +++ b/src/main.rs @@ -74,8 +74,9 @@ async fn run() -> Result<(), Error> { .service(web::overboard::overboard) .service(web::overboard_catalog::overboard_catalog) .service(web::page::page) - .service(web::thread_json::thread_json) + .service(web::search::search) .service(web::thread::thread) + .service(web::thread_json::thread_json) .service(web::actions::appeal_ban::appeal_ban) .service(web::actions::create_post::create_post) .service(web::actions::edit_posts::edit_posts) diff --git a/src/web/actions/create_post.rs b/src/web/actions/create_post.rs index 7ea635a..b138249 100644 --- a/src/web/actions/create_post.rs +++ b/src/web/actions/create_post.rs @@ -25,12 +25,14 @@ use crate::{ pub struct PostForm { pub board: Text, pub thread: Option>, + #[multipart(rename = "post_name")] pub name: Text, pub email: Text, pub content: Text, #[multipart(rename = "files[]")] pub files: Vec, pub spoiler_files: Option>, + #[multipart(rename = "post_password")] pub password: Text, pub captcha_id: Option>, pub captcha_solution: Option>, @@ -260,9 +262,11 @@ pub async fn create_post( let name_cookie = Cookie::build("name", name_raw).path("/").finish(); let password_cookie = Cookie::build("password", password_raw).path("/").finish(); + let email_cookie = Cookie::build("email", email_raw).path("/").finish(); res.cookie(name_cookie); res.cookie(password_cookie); + res.cookie(email_cookie); let res = if noko { res.append_header(("Location", post.post_url().as_str())) diff --git a/src/web/actions/user_post_actions.rs b/src/web/actions/user_post_actions.rs index 57360be..6c9f580 100644 --- a/src/web/actions/user_post_actions.rs +++ b/src/web/actions/user_post_actions.rs @@ -23,6 +23,7 @@ pub struct UserPostActionsForm { pub remove_posts: Option, pub remove_files: Option, pub toggle_spoiler: Option, + #[serde(rename = "post_password")] pub password: String, } diff --git a/src/web/login.rs b/src/web/login.rs index d650fbb..f717a7e 100755 --- a/src/web/login.rs +++ b/src/web/login.rs @@ -32,7 +32,6 @@ pub async fn login_get(ctx: Data, req: HttpRequest) -> Result, + boards: HashMap, + query: String, + posts: Vec, + page: i64, +} + +#[derive(Deserialize)] +pub struct SearchQuery { + board: Option, + query: String, + page: Option, +} + +#[get("/search")] +pub async fn search( + ctx: Data, + req: HttpRequest, + Query(query): Query, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + let board_opt = if let Some(board) = query.board { + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + Some(board) + } else { + None + }; + + let boards = if board_opt.is_none() { + Board::read_all_map(&ctx).await? + } else { + HashMap::new() + }; + + let page = query.page.unwrap_or(1); + + if page <= 0 { + return Err(NekrochanError::InvalidPageError); + } + + let query = query.query; + + if query.is_empty() || query.len() > 256 { + return Err(NekrochanError::QueryFormatError); + } + + let posts = if let Some(board) = &board_opt { + Post::read_by_query(&ctx, board, query.clone(), page).await? + } else { + Post::read_by_query_overboard(&ctx, query.clone(), page).await? + }; + + let template = SearchTemplate { + tcx, + board_opt, + boards, + query, + posts, + page, + }; + + template_response(&template) +} diff --git a/src/web/tcx.rs b/src/web/tcx.rs index b435c3c..e721af0 100755 --- a/src/web/tcx.rs +++ b/src/web/tcx.rs @@ -1,5 +1,6 @@ use actix_web::HttpRequest; use redis::{AsyncCommands, Commands, Connection}; +use sqlx::query_as; use std::{ collections::HashSet, net::{IpAddr, Ipv4Addr}, @@ -18,6 +19,7 @@ pub struct TemplateCtx { pub perms: PermissionWrapper, pub ip: IpAddr, pub yous: HashSet, + pub report_count: Option, } impl TemplateCtx { @@ -37,6 +39,20 @@ impl TemplateCtx { let account = account.map(|account| account.username); + let report_count = if perms.owner() || perms.reports() { + let count: Option> = query_as("SELECT SUM(jsonb_array_length(reports)) FROM overboard WHERE reports != '[]'::jsonb") + .fetch_optional(ctx.db()) + .await + .ok(); + + match count { + Some(Some((count,))) if count != 0 => Some(count), + _ => None, + } + } else { + None + }; + let tcx = Self { cfg, boards, @@ -44,6 +60,7 @@ impl TemplateCtx { ip, yous, account, + report_count, }; Ok(tcx) diff --git a/static/favicon.ico b/static/favicon.ico index b13770b..6585a85 100644 Binary files a/static/favicon.ico and b/static/favicon.ico differ diff --git a/static/js/autofill.js b/static/js/autofill.js index c2dcc1b..cb3a8b4 100644 --- a/static/js/autofill.js +++ b/static/js/autofill.js @@ -1,50 +1,52 @@ $(function () { let name = get_cookie("name"); let password = get_cookie("password"); + let email = get_cookie("email"); if (password === "") { password = generate_password(); set_cookie("password", password); } - $('input[name="name"]').attr("value", name); - $('input[name="password"]').attr("value", password); + $('input[name="post_name"]').attr("value", name); + $('input[name="post_password"]').attr("value", password); + $('input[name="email"]').attr("value", email); + + function generate_password() { + let chars = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let password_length = 8; + let password = ""; + + for (let i = 0; i <= password_length; i++) { + let random_number = Math.floor(Math.random() * chars.length); + password += chars.substring(random_number, random_number + 1); + } + + return password; + } + + function get_cookie(cname) { + let name = cname + "="; + let decodedCookie = decodeURIComponent(document.cookie); + let ca = decodedCookie.split(";"); + + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + + while (c.charAt(0) == " ") { + c = c.substring(1); + } + + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + + return ""; + } + + function set_cookie(cname, cvalue) { + document.cookie = `${cname}=${cvalue};path=/`; + } }); - -function generate_password() { - let chars = - "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - let password_length = 8; - let password = ""; - - for (let i = 0; i <= password_length; i++) { - let random_number = Math.floor(Math.random() * chars.length); - password += chars.substring(random_number, random_number + 1); - } - - return password; -} - -function get_cookie(cname) { - let name = cname + "="; - let decodedCookie = decodeURIComponent(document.cookie); - let ca = decodedCookie.split(";"); - - for (let i = 0; i < ca.length; i++) { - let c = ca[i]; - - while (c.charAt(0) == " ") { - c = c.substring(1); - } - - if (c.indexOf(name) == 0) { - return c.substring(name.length, c.length); - } - } - - return ""; -} - -function set_cookie(cname, cvalue) { - document.cookie = `${cname}=${cvalue};path=/`; -} diff --git a/static/js/post-form.js b/static/js/post-form.js index b3dbb57..b58324c 100644 --- a/static/js/post-form.js +++ b/static/js/post-form.js @@ -30,10 +30,10 @@ $(function () { if (saved_top) { form.css("top", saved_top); } - + if (saved_left) { form.css("left", saved_left); - form.css("right", "auto") + form.css("right", "auto"); } handle.on("mousedown", start); @@ -139,11 +139,12 @@ $(function () { return; } - if (rect.right > document.documentElement.clientWidth) { + if (Math.floor(rect.right) > document.documentElement.clientWidth) { form.css("left", 0); + form.css("right", "auto"); } - if (rect.bottom > document.documentElement.clientHeight) { + if (Math.floor(rect.bottom) > document.documentElement.clientHeight) { form.css("top", 0); } diff --git a/templates/base.html b/templates/base.html index f899185..8638622 100755 --- a/templates/base.html +++ b/templates/base.html @@ -9,14 +9,6 @@ - - - - - - - - {% block scripts %}{% endblock %}
@@ -39,6 +31,9 @@ {% endfor %} + {% if let Some(report_count) = tcx.report_count %} + Hlášení: {{ report_count }} + {% endif %} {% if tcx.account.is_some() %} odhlásit se @@ -57,5 +52,12 @@
+ + + + + + + {% block scripts %}{% endblock %} diff --git a/templates/board-catalog.html b/templates/board-catalog.html index fad2d10..e4b3657 100755 --- a/templates/board-catalog.html +++ b/templates/board-catalog.html @@ -16,6 +16,20 @@
+
+ + + + + + +
+ + + +
+
+
{% for thread in threads %} diff --git a/templates/board.html b/templates/board.html index 735a4c3..6087069 100755 --- a/templates/board.html +++ b/templates/board.html @@ -8,8 +8,8 @@ {% block theme %}{{ board.config.0.board_theme }}{% endblock %} {% block title %}/{{ board.id }}/ - {{ board.name }}{% endblock %} {% block scripts %} - - + + {% endblock %} {% block content %} diff --git a/templates/ip-posts.html b/templates/ip-posts.html index 06580ae..9753ea4 100644 --- a/templates/ip-posts.html +++ b/templates/ip-posts.html @@ -7,7 +7,10 @@ {% block title %}Příspěvky od [{{ ip }}]{% endblock %} {% block content %} -

Příspěvky od [{{ ip }}]

+
+ +

Příspěvky od [{{ ip }}]

+

@@ -19,7 +22,7 @@ {% endfor %}

- {% call static_pagination::static_pagination("/ip-posts/{}"|format(ip), page) %} + {% call static_pagination::static_pagination("/ip-posts/{}"|format(ip), page, false) %}
{% call post_actions::post_actions() %} diff --git a/templates/login.html b/templates/login.html index 609c7c0..7f4d12a 100755 --- a/templates/login.html +++ b/templates/login.html @@ -13,7 +13,7 @@ Heslo - + diff --git a/templates/macros/post-actions.html b/templates/macros/post-actions.html index 3292c8f..5da477f 100644 --- a/templates/macros/post-actions.html +++ b/templates/macros/post-actions.html @@ -29,7 +29,7 @@ Heslo - + diff --git a/templates/macros/post-form.html b/templates/macros/post-form.html index 520fdb8..1d1e3ae 100644 --- a/templates/macros/post-form.html +++ b/templates/macros/post-form.html @@ -1,5 +1,5 @@ {% macro post_form(board, reply, reply_to) %} -
+ {% if reply %} @@ -18,13 +18,13 @@ Jméno - + Email - + @@ -91,7 +91,7 @@ Heslo - + {% if reply %} diff --git a/templates/macros/static-pagination.html b/templates/macros/static-pagination.html index 98e2276..4ef0118 100644 --- a/templates/macros/static-pagination.html +++ b/templates/macros/static-pagination.html @@ -1,13 +1,22 @@ -{% macro static_pagination(base, current) %} +{% macro static_pagination(base, current, chain) %} {% endmacro %} diff --git a/templates/news.html b/templates/news.html index 4b7e93e..36a6a6e 100644 --- a/templates/news.html +++ b/templates/news.html @@ -4,7 +4,10 @@ {% block content %}
-

Novinky

+
+ +

Novinky

+
{% for newspost in news %}

diff --git a/templates/overboard-catalog.html b/templates/overboard-catalog.html index 6139abe..0ac4dd6 100755 --- a/templates/overboard-catalog.html +++ b/templates/overboard-catalog.html @@ -15,6 +15,19 @@


+ + + + + + +
+ + + +
+
+
{% for thread in threads %} diff --git a/templates/search.html b/templates/search.html new file mode 100644 index 0000000..b958392 --- /dev/null +++ b/templates/search.html @@ -0,0 +1,47 @@ +{% import "./macros/post-actions.html" as post_actions %} +{% import "./macros/post.html" as post %} +{% import "./macros/static-pagination.html" as static_pagination %} + +{% extends "base.html" %} + +{% block title %} +Vyhledávání ({% if let Some(board) = board_opt %}/{{ board.id }}/{% else %}nadnástěnka{% endif %}) +{% endblock %} + +{% block content %} +
+ +

+ Výsledky pro "{{ query }}" ( + {% if let Some(board) = board_opt %} + /{{ board.id }}/ + {% else %} + nadnástěnka + {% endif %} + ) +

+
+
+ +
+ {% for post in posts %} + {% if let Some(board) = board_opt %} + {% call post::post(board, post, true) %} + {% else %} + Příspěvek z /{{ post.board }}/ +
+ {% call post::post(boards[post.board.as_str()], post, true) %} + {% endif %} +
+ {% endfor %} +
+
+ {% if let Some(board) = board_opt %} + {% call static_pagination::static_pagination("/search?board={}&query={}"|format(board.id, query|urlencode_strict), page, true) %} + {% else %} + {% call static_pagination::static_pagination("/search?query={}"|format(query|urlencode_strict), page, true) %} + {% endif %} +
+ {% call post_actions::post_actions() %} + +{% endblock %} diff --git a/templates/staff/accounts.html b/templates/staff/accounts.html index bb6eca2..f898bb6 100755 --- a/templates/staff/accounts.html +++ b/templates/staff/accounts.html @@ -45,7 +45,7 @@ Heslo - + diff --git a/templates/staff/boards.html b/templates/staff/boards.html index cd079c3..935adb4 100755 --- a/templates/staff/boards.html +++ b/templates/staff/boards.html @@ -35,11 +35,7 @@ {% if tcx.perms.owner() %} - {% endif %} - -
- - {% if tcx.perms.owner() || tcx.perms.board_config() %} +
diff --git a/templates/staff/reports.html b/templates/staff/reports.html index 125cc88..bb643e9 100755 --- a/templates/staff/reports.html +++ b/templates/staff/reports.html @@ -33,7 +33,7 @@
Jméno

{% endfor %} - {% call static_pagination::static_pagination("/staff/reports", page) %} + {% call static_pagination::static_pagination("/staff/reports", page, false) %}
{% call post_actions::post_actions() %} diff --git a/templates/thread.html b/templates/thread.html index 863a4a7..5c715b9 100644 --- a/templates/thread.html +++ b/templates/thread.html @@ -8,9 +8,9 @@ {% block title %}/{{ board.id }}/ - {{ thread.content_nomarkup|inline_post }}{% endblock %} {% block scripts %} - - - + + + {% endblock %} {% block content %}