diff --git a/Cargo.lock b/Cargo.lock index ed9bb2f..4c1c355 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -586,6 +586,28 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "chrono-tz" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d7b79e99bfaa0d47da0687c43aa3b7381938a62ad3a6498599039321f660b7" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "cipher" version = "0.2.5" @@ -1786,6 +1808,7 @@ dependencies = [ "askama", "captcha", "chrono", + "chrono-tz", "dotenv", "encoding", "enumflags2", @@ -1953,6 +1976,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "paste" version = "1.0.13" @@ -1984,6 +2016,44 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.2" @@ -2487,6 +2557,12 @@ dependencies = [ "time", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.8" diff --git a/Cargo.toml b/Cargo.toml index 53294c4..acb9581 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ askama = "0.12.0" anyhow = "1.0.71" captcha = "0.0.9" chrono = { version = "0.4.31", features = ["serde", "unstable-locales"] } +chrono-tz = "0.8.5" dotenv = "0.15.0" enumflags2 = "0.7.7" encoding = "0.2.33" diff --git a/src/db/banner.rs b/src/db/banner.rs index 7452939..ab2b68f 100644 --- a/src/db/banner.rs +++ b/src/db/banner.rs @@ -38,7 +38,7 @@ impl Banner { Ok(banners) } - pub async fn read_random(ctx: &Ctx) -> Result, NekrochanError> { + pub async fn read_random(ctx: &Ctx) -> Result, NekrochanError> { let banner: Option = ctx.cache().zrandmember("banners", None).await?; let banner = match banner { diff --git a/src/db/local_stats.rs b/src/db/local_stats.rs index a17bdb5..1f04679 100644 --- a/src/db/local_stats.rs +++ b/src/db/local_stats.rs @@ -1,7 +1,7 @@ use sqlx::query_as; -use crate::{error::NekrochanError, ctx::Ctx}; use super::models::LocalStats; +use crate::{ctx::Ctx, error::NekrochanError}; impl LocalStats { pub async fn read(ctx: &Ctx) -> Result { @@ -27,4 +27,4 @@ impl LocalStats { Ok(stats) } -} \ No newline at end of file +} diff --git a/src/db/newspost.rs b/src/db/newspost.rs index c1410cb..93b2417 100644 --- a/src/db/newspost.rs +++ b/src/db/newspost.rs @@ -8,11 +8,13 @@ impl NewsPost { ctx: &Ctx, title: String, content: String, + content_nomarkup: String, author: String, ) -> Result { - let newspost = query_as("INSERT INTO news (title, content, author) VALUES ($1, $2, $3)") + let newspost = query_as("INSERT INTO news (title, content, content_nomarkup, author) VALUES ($1, $2, $3, $4) RETURNING *") .bind(title) .bind(content) + .bind(content_nomarkup) .bind(author) .fetch_one(ctx.db()) .await?; @@ -48,12 +50,12 @@ impl NewsPost { pub async fn update( &self, ctx: &Ctx, - title: String, content: String, + content_nomarkup: String, ) -> Result<(), NekrochanError> { - query("UPDATE news SET title = $1, content = $2 WHERE id = $3") - .bind(title) + query("UPDATE news SET content = $1, content_nomarkup = $2 WHERE id = $3") .bind(content) + .bind(content_nomarkup) .bind(self.id) .execute(ctx.db()) .await?; diff --git a/src/error.rs b/src/error.rs index 516d8d0..715860b 100755 --- a/src/error.rs +++ b/src/error.rs @@ -8,8 +8,12 @@ pub enum NekrochanError { AccountNotFound(String), #[error("Tento ban už byl odvolán.")] AlreadyAppealedError, + #[error("Odvolání můsí mít 1-1000 znaků.")] + BanAppealFormatError, #[error("Žádný takový ban pro tuto IP adresu neexistuje.")] BanNotFound, + #[error("Důvod banu musí mít 1-200 znaků.")] + BanReasonFormatError, #[error("Nástěnka /{}/ je uzamčená.", .0)] BoardLockError(String), #[error("Jméno nástěnky musí mít 1-32 znaků.")] @@ -52,6 +56,10 @@ pub enum NekrochanError { InvalidAuthError, #[error("Neplatná strana.")] InvalidPageError, + #[error("Obsah musí mít 1-20000 znaků.")] + NewsContentFormatError, + #[error("Titulek musí mít 1-100 znaků.")] + NewsTitleFormatError, #[error("Příspěvek musí mít obsah.")] NoContentError, #[error("Příspěvek musí mít soubor.")] @@ -72,6 +80,8 @@ pub enum NekrochanError { PostNotFound(String, i64), #[error("Vlákno dosáhlo limitu odpovědí.")] ReplyLimitError, + #[error("Hlášení můsí mít 1-200 znaků.")] + ReportFormatError, #[error("Nelze vytvořit odpověď na odpověď.")] ReplyReplyError, #[error("Na této nástěnce se musí vyplnit CAPTCHA.")] @@ -204,7 +214,9 @@ impl ResponseError for NekrochanError { match self { NekrochanError::AccountNotFound(_) => StatusCode::NOT_FOUND, NekrochanError::AlreadyAppealedError => StatusCode::BAD_REQUEST, + NekrochanError::BanAppealFormatError => StatusCode::BAD_REQUEST, NekrochanError::BanNotFound => StatusCode::NOT_FOUND, + NekrochanError::BanReasonFormatError => StatusCode::BAD_REQUEST, NekrochanError::BoardLockError(_) => StatusCode::FORBIDDEN, NekrochanError::BoardNameFormatError => StatusCode::BAD_REQUEST, NekrochanError::BoardNotFound(_) => StatusCode::NOT_FOUND, @@ -226,6 +238,8 @@ impl ResponseError for NekrochanError { NekrochanError::InternalError => StatusCode::INTERNAL_SERVER_ERROR, NekrochanError::InvalidAuthError => StatusCode::UNAUTHORIZED, NekrochanError::InvalidPageError => StatusCode::BAD_REQUEST, + NekrochanError::NewsContentFormatError => StatusCode::BAD_REQUEST, + NekrochanError::NewsTitleFormatError => StatusCode::BAD_REQUEST, NekrochanError::NoContentError => StatusCode::BAD_REQUEST, NekrochanError::NoFileError => StatusCode::BAD_REQUEST, NekrochanError::NoPostsError => StatusCode::BAD_REQUEST, @@ -237,6 +251,7 @@ impl ResponseError for NekrochanError { NekrochanError::PostNotFound(_, _) => StatusCode::NOT_FOUND, NekrochanError::ReplyLimitError => StatusCode::FORBIDDEN, NekrochanError::ReplyReplyError => StatusCode::BAD_REQUEST, + NekrochanError::ReportFormatError => StatusCode::BAD_REQUEST, NekrochanError::RequiredCaptchaError => StatusCode::UNAUTHORIZED, NekrochanError::SolvedCaptchaError => StatusCode::BAD_REQUEST, NekrochanError::ThreadLockError => StatusCode::FORBIDDEN, diff --git a/src/filters.rs b/src/filters.rs index 9c71e94..b9a7b6c 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,4 +1,5 @@ -use chrono::{DateTime, Locale, Utc}; +use chrono::{DateTime, Locale, TimeZone, Utc}; +use chrono_tz::Europe::Prague; use lazy_static::lazy_static; use regex::{Captures, Regex}; use std::{collections::HashSet, fmt::Display}; @@ -66,7 +67,9 @@ pub fn czech_humantime(time: &DateTime) -> askama::Result { Ok(time) } -pub fn czech_datetime(time: &DateTime) -> askama::Result { +pub fn czech_datetime(utc: &DateTime) -> askama::Result { + let time = Prague.from_utc_datetime(&utc.naive_utc()); + let time = time .format_localized("%d.%m.%Y (%a) %H:%M:%S", Locale::cs_CZ) .to_string(); diff --git a/src/lib.rs b/src/lib.rs index c346061..64755f0 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,11 +13,11 @@ pub mod ctx; pub mod db; pub mod error; pub mod files; -pub mod schedule; pub mod filters; pub mod markup; pub mod perms; pub mod qsform; +pub mod schedule; pub mod trip; pub mod web; diff --git a/src/main.rs b/src/main.rs index f321364..2d178c6 100755 --- a/src/main.rs +++ b/src/main.rs @@ -67,6 +67,7 @@ async fn run() -> Result<(), Error> { .service(web::login::login_get) .service(web::login::login_post) .service(web::logout::logout) + .service(web::news::news) .service(web::overboard::overboard) .service(web::overboard_catalog::overboard_catalog) .service(web::thread::thread) @@ -82,17 +83,22 @@ async fn run() -> Result<(), Error> { .service(web::staff::banners::banners) .service(web::staff::board_config::board_config) .service(web::staff::boards::boards) + .service(web::staff::edit_news::edit_news) + .service(web::staff::news::news) .service(web::staff::permissions::permissions) .service(web::staff::reports::reports) .service(web::staff::actions::add_banners::add_banners) .service(web::staff::actions::change_password::change_password) .service(web::staff::actions::create_account::create_account) .service(web::staff::actions::create_board::create_board) + .service(web::staff::actions::create_news::create_news) .service(web::staff::actions::delete_account::delete_account) + .service(web::staff::actions::edit_news::edit_news) .service(web::staff::actions::remove_accounts::remove_accounts) .service(web::staff::actions::remove_banners::remove_banners) .service(web::staff::actions::remove_bans::remove_bans) .service(web::staff::actions::remove_boards::remove_boards) + .service(web::staff::actions::remove_news::remove_news) .service(web::staff::actions::transfer_ownership::transfer_ownership) .service(web::staff::actions::update_board_config::update_board_config) .service(web::staff::actions::update_boards::update_boards) diff --git a/src/markup.rs b/src/markup.rs index ed04776..7b74d09 100644 --- a/src/markup.rs +++ b/src/markup.rs @@ -78,7 +78,7 @@ pub fn parse_name( Some(capcode) => { let capcode: String = capcode.as_str().trim().into(); - if capcode.is_empty() { + if capcode.is_empty() || !(perms.owner() || perms.custom_capcodes()) { Some(capcode_fallback(perms.owner())) } else { if capcode.len() > 32 { @@ -106,38 +106,44 @@ fn capcode_fallback(owner: bool) -> String { pub async fn markup( ctx: &Ctx, - board: &String, + board: Option, op: Option, text: &str, ) -> Result { - let text = escape_html(&text); + let text = escape_html(text); - let quoted_posts = get_quoted_posts(ctx, board, &text).await?; + let text = if let Some(board) = board { + let quoted_posts = get_quoted_posts(ctx, &board, &text).await?; - let text = QUOTE_REGEX.replace_all(&text, |captures: &Captures| { - let id_raw = &captures[1]; + let text = QUOTE_REGEX.replace_all(&text, |captures: &Captures| { + let id_raw = &captures[1]; - let Ok(id) = id_raw.parse() else { + let Ok(id) = id_raw.parse() else { return format!(">>{id_raw}"); }; - let post = quoted_posts.get(&id); + let post = quoted_posts.get(&id); - if let Some(post) = post { - format!( - ">>{}{}", - post.post_url(), - post.id, - if op == Some(post.id) { - " (OP)" - } else { - "" - } - ) - } else { - format!(">>{id}") - } - }); + if let Some(post) = post { + format!( + ">>{}{}", + post.post_url(), + post.id, + if op == Some(post.id) { + " (OP)" + } else { + "" + } + ) + } else { + format!(">>{id}") + } + }); + + text.to_string() + } else { + text + }; let text = GREENTEXT_REGEX.replace_all(&text, ">$1"); let text = ORANGETEXT_REGEX.replace_all(&text, "<$1"); diff --git a/src/perms.rs b/src/perms.rs index 817f617..b6af074 100755 --- a/src/perms.rs +++ b/src/perms.rs @@ -7,6 +7,7 @@ pub enum Permissions { EditPosts, ManagePosts, Capcodes, + CustomCapcodes, StaffLog, Reports, Bans, @@ -17,6 +18,7 @@ pub enum Permissions { BypassBoardLock, BypassThreadLock, BypassCaptcha, + BypassAntispam, } pub struct PermissionWrapper(BitFlags, bool); @@ -48,6 +50,10 @@ impl PermissionWrapper { self.0.contains(Permissions::Capcodes) } + pub fn custom_capcodes(&self) -> bool { + self.0.contains(Permissions::CustomCapcodes) + } + pub fn staff_log(&self) -> bool { self.0.contains(Permissions::StaffLog) } @@ -87,4 +93,8 @@ impl PermissionWrapper { pub fn bypass_captcha(&self) -> bool { self.0.contains(Permissions::BypassCaptcha) } + + pub fn bypass_antispam(&self) -> bool { + self.0.contains(Permissions::BypassAntispam) + } } diff --git a/src/web/actions/appeal_ban.rs b/src/web/actions/appeal_ban.rs index b6777a6..e6e02f9 100644 --- a/src/web/actions/appeal_ban.rs +++ b/src/web/actions/appeal_ban.rs @@ -44,7 +44,12 @@ pub async fn appeal_ban( return Err(NekrochanError::UnappealableError); } - let appeal = form.appeal.trim().into(); + let appeal: String = form.appeal.trim().into(); + + if appeal.is_empty() || appeal.len() > 1000 { + return Err(NekrochanError::BanAppealFormatError); + } + ban.update_appeal(&ctx, appeal).await?; template_response(&ActionTemplate { diff --git a/src/web/actions/create_post.rs b/src/web/actions/create_post.rs index e4364bf..03151ea 100644 --- a/src/web/actions/create_post.rs +++ b/src/web/actions/create_post.rs @@ -168,7 +168,7 @@ pub async fn create_post( let content = markup( &ctx, - &board.id, + Some(board.id.clone()), thread.as_ref().map(|t| t.id), &content_nomarkup, ) diff --git a/src/web/actions/edit_posts.rs b/src/web/actions/edit_posts.rs index d342ef5..cb2f836 100644 --- a/src/web/actions/edit_posts.rs +++ b/src/web/actions/edit_posts.rs @@ -38,7 +38,13 @@ pub async fn edit_posts( for (key, content_nomarkup) in edits { let post = &posts[&key]; let content_nomarkup = content_nomarkup.trim(); - let content = markup(&ctx, &post.board, post.thread, content_nomarkup).await?; + let content = markup( + &ctx, + Some(post.board.clone()), + post.thread, + content_nomarkup, + ) + .await?; post.update_content(&ctx, content, content_nomarkup.into()) .await?; diff --git a/src/web/actions/report_posts.rs b/src/web/actions/report_posts.rs index a962dc3..220eeeb 100644 --- a/src/web/actions/report_posts.rs +++ b/src/web/actions/report_posts.rs @@ -46,6 +46,10 @@ pub async fn report_posts( let reason = form.report_reason.trim(); + if reason.is_empty() || reason.len() > 200 { + return Err(NekrochanError::ReportFormatError); + } + for post in &posts { let board = &boards[&post.board]; diff --git a/src/web/actions/staff_post_actions.rs b/src/web/actions/staff_post_actions.rs index 2e01e43..5593192 100644 --- a/src/web/actions/staff_post_actions.rs +++ b/src/web/actions/staff_post_actions.rs @@ -139,7 +139,12 @@ pub async fn staff_post_actions( }; let ip_range = IpNetwork::new(post.ip, prefix)?; - let reason = ban_reason.trim().into(); + let reason: String = ban_reason.trim().into(); + + if reason.is_empty() || reason.len() > 200 { + return Err(NekrochanError::BanReasonFormatError); + } + let appealable = form.unappealable_ban.is_none(); let expires = if ban_duration == 0 { diff --git a/src/web/edit_posts.rs b/src/web/edit_posts.rs index 9a5c320..3a53331 100644 --- a/src/web/edit_posts.rs +++ b/src/web/edit_posts.rs @@ -12,6 +12,7 @@ use crate::{ #[derive(Deserialize)] pub struct EditPostsForm { + #[serde(default)] pub posts: Vec, } diff --git a/src/web/index.rs b/src/web/index.rs index abb0e7c..7c5a97f 100755 --- a/src/web/index.rs +++ b/src/web/index.rs @@ -32,7 +32,12 @@ pub async fn index(ctx: Data, req: HttpRequest) -> Result, +} + +#[get("/news")] +pub async fn news(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let news = NewsPost::read_all(&ctx).await?; + let template = NewsTemplate { tcx, news }; + + template_response(&template) +} diff --git a/src/web/staff/actions/create_news.rs b/src/web/staff/actions/create_news.rs new file mode 100644 index 0000000..2fba6db --- /dev/null +++ b/src/web/staff/actions/create_news.rs @@ -0,0 +1,48 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::NewsPost, error::NekrochanError, markup::markup, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct CreateNewsForm { + title: String, + content: String, +} + +#[post("/staff/actions/create-news")] +pub async fn create_news( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().news()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let title = form.title.trim().to_owned(); + let content = form.content.trim().to_owned(); + + if title.is_empty() || title.len() > 100 { + return Err(NekrochanError::NewsTitleFormatError); + } + + if content.is_empty() || content.len() > 10000 { + return Err(NekrochanError::NewsContentFormatError); + } + + let content_nomarkup = content; + let content = markup(&ctx, None, None, &content_nomarkup).await?; + + NewsPost::create(&ctx, title, content, content_nomarkup, account.username).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/news")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/edit_news.rs b/src/web/staff/actions/edit_news.rs new file mode 100644 index 0000000..2347da7 --- /dev/null +++ b/src/web/staff/actions/edit_news.rs @@ -0,0 +1,70 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use std::{collections::HashMap, fmt::Write}; + +use crate::{ + ctx::Ctx, + db::models::NewsPost, + error::NekrochanError, + markup::markup, + qsform::QsForm, + web::{actions::ActionTemplate, tcx::TemplateCtx, template_response}, +}; + +#[post("/staff/actions/edit-news")] +pub async fn edit_news( + ctx: Data, + req: HttpRequest, + QsForm(edits): QsForm>, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.news()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let mut news = Vec::new(); + + for id in edits.keys() { + if let Some(newspost) = NewsPost::read(&ctx, *id).await? { + news.push(newspost); + } + } + + let news = news + .into_iter() + .map(|newspost| (newspost.id, newspost)) + .collect::>(); + + let mut response = String::new(); + let mut news_edited = 0; + + for (id, content_nomarkup) in edits { + let newspost = &news[&id]; + + if !tcx.perms.owner() && tcx.account != Some(newspost.author.clone()) { + writeln!( + &mut response, + "[Chyba] pouze vlastník nebo autor může upravit novinky." + ) + .ok(); + + continue; + } + + let content_nomarkup = content_nomarkup.trim(); + let content = markup(&ctx, None, None, content_nomarkup).await?; + + newspost + .update(&ctx, content, content_nomarkup.into()) + .await?; + news_edited += 1; + } + + if news_edited != 0 { + writeln!(&mut response, "[Úspěch] Upraveny novinky: {news_edited}").ok(); + } + + let template = ActionTemplate { tcx, response }; + + template_response(&template) +} diff --git a/src/web/staff/actions/mod.rs b/src/web/staff/actions/mod.rs index 2921538..cb33f4f 100755 --- a/src/web/staff/actions/mod.rs +++ b/src/web/staff/actions/mod.rs @@ -2,11 +2,14 @@ pub mod add_banners; pub mod change_password; pub mod create_account; pub mod create_board; +pub mod create_news; pub mod delete_account; +pub mod edit_news; pub mod remove_accounts; pub mod remove_banners; pub mod remove_bans; pub mod remove_boards; +pub mod remove_news; pub mod transfer_ownership; pub mod update_board_config; pub mod update_boards; diff --git a/src/web/staff/actions/remove_news.rs b/src/web/staff/actions/remove_news.rs new file mode 100644 index 0000000..f0aedef --- /dev/null +++ b/src/web/staff/actions/remove_news.rs @@ -0,0 +1,65 @@ +use std::fmt::Write; + +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, + db::models::NewsPost, + error::NekrochanError, + qsform::QsForm, + web::{actions::ActionTemplate, template_response, TemplateCtx}, +}; + +#[derive(Deserialize)] +pub struct RemoveNewsForm { + #[serde(default)] + pub news: Vec, +} + +#[post("/staff/actions/remove-news")] +pub async fn remove_news( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.news()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let mut news = Vec::new(); + + for id in form.news { + if let Some(newspost) = NewsPost::read(&ctx, id).await? { + news.push(newspost); + } + } + + let mut response = String::new(); + let mut news_removed = 0; + + for newspost in news { + if !tcx.perms.owner() && tcx.account != Some(newspost.author.clone()) { + writeln!( + &mut response, + "[Chyba] pouze vlastník nebo autor může odstranit novinky." + ) + .ok(); + + continue; + } + + newspost.delete(&ctx).await?; + news_removed += 1; + } + + if news_removed != 0 { + writeln!(&mut response, "[Úspěch] Odstraněny novinky: {news_removed}").ok(); + } + + let template = ActionTemplate { tcx, response }; + + template_response(&template) +} diff --git a/src/web/staff/actions/update_permissions.rs b/src/web/staff/actions/update_permissions.rs index 39c05d8..20e9fcc 100755 --- a/src/web/staff/actions/update_permissions.rs +++ b/src/web/staff/actions/update_permissions.rs @@ -13,15 +13,18 @@ pub struct UpdatePermissionsForm { edit_posts: Option, manage_posts: Option, capcodes: Option, + custom_capcodes: Option, staff_log: Option, reports: Option, bans: Option, banners: Option, + news: Option, board_config: Option, bypass_bans: Option, bypass_board_lock: Option, bypass_thread_lock: Option, bypass_captcha: Option, + bypass_antispam: Option, } #[post("/staff/actions/update-permissions")] @@ -55,6 +58,10 @@ pub async fn update_permissions( permissions |= Permissions::Capcodes; } + if form.custom_capcodes.is_some() { + permissions |= Permissions::CustomCapcodes; + } + if form.staff_log.is_some() { permissions |= Permissions::StaffLog; } @@ -75,6 +82,10 @@ pub async fn update_permissions( permissions |= Permissions::BoardConfig; } + if form.news.is_some() { + permissions |= Permissions::News; + } + if form.bypass_bans.is_some() { permissions |= Permissions::BypassBans; } @@ -91,6 +102,10 @@ pub async fn update_permissions( permissions |= Permissions::BypassCaptcha; } + if form.bypass_antispam.is_some() { + permissions |= Permissions::BypassAntispam; + } + updated_account .update_permissions(&ctx, permissions.bits()) .await?; diff --git a/src/web/staff/edit_news.rs b/src/web/staff/edit_news.rs new file mode 100644 index 0000000..d7f9865 --- /dev/null +++ b/src/web/staff/edit_news.rs @@ -0,0 +1,49 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use askama::Template; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, + db::models::NewsPost, + error::NekrochanError, + filters, + qsform::QsForm, + web::{template_response, TemplateCtx}, +}; + +#[derive(Deserialize)] +pub struct EditNewsForm { + pub news: Vec, +} + +#[derive(Template)] +#[template(path = "staff/edit-news.html")] +struct EditNewsTemplate { + tcx: TemplateCtx, + news: Vec, +} + +#[post("/staff/edit-news")] +pub async fn edit_news( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.news()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let mut news = Vec::new(); + + for id in form.news { + if let Some(newspost) = NewsPost::read(&ctx, id).await? { + news.push(newspost); + } + } + + let template = EditNewsTemplate { tcx, news }; + + template_response(&template) +} diff --git a/src/web/staff/mod.rs b/src/web/staff/mod.rs index d1425f1..f61de2e 100755 --- a/src/web/staff/mod.rs +++ b/src/web/staff/mod.rs @@ -5,5 +5,7 @@ pub mod banners; pub mod bans; pub mod board_config; pub mod boards; +pub mod edit_news; +pub mod news; pub mod permissions; pub mod reports; diff --git a/src/web/staff/news.rs b/src/web/staff/news.rs new file mode 100644 index 0000000..d01e5cb --- /dev/null +++ b/src/web/staff/news.rs @@ -0,0 +1,25 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::NewsPost, + error::NekrochanError, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "staff/news.html")] +struct NewsTemplate { + tcx: TemplateCtx, + news: Vec, +} + +#[get("/staff/news")] +pub async fn news(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let news = NewsPost::read_all(&ctx).await?; + let template = NewsTemplate { tcx, news }; + + template_response(&template) +} diff --git a/src/web/tcx.rs b/src/web/tcx.rs index 950af91..03293f6 100755 --- a/src/web/tcx.rs +++ b/src/web/tcx.rs @@ -14,7 +14,7 @@ use crate::{ pub struct TemplateCtx { pub cfg: Cfg, pub boards: Vec, - pub logged_in: bool, + pub account: Option, pub perms: PermissionWrapper, pub name: Option, pub password: String, @@ -28,7 +28,6 @@ impl TemplateCtx { let boards = ctx.cache().lrange("board_ids", 0, -1).await?; let account = account_from_auth_opt(ctx, req).await?; - let logged_in = account.is_some(); let perms = match &account { Some(account) => account.perms(), @@ -50,15 +49,17 @@ impl TemplateCtx { let (ip, _) = ip_from_req(req)?; let yous = ctx.cache().zrange(format!("by_ip:{ip}"), 0, -1).await?; + let account = account.map(|account| account.username); + let tcx = Self { cfg, boards, - logged_in, perms, name, password, ip, yous, + account, }; Ok(tcx) diff --git a/static/style.css b/static/style.css index f8421ea..9a0d66d 100755 --- a/static/style.css +++ b/static/style.css @@ -54,7 +54,7 @@ summary { padding: 0; } -.edit-post { +.edit-box { display: block; width: 100%; } @@ -65,7 +65,7 @@ summary { .form-table textarea, .form-table select, .input-wrapper, -.edit-post { +.edit-box { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; @@ -85,15 +85,11 @@ summary { } .form-table textarea, -.edit-post { +.edit-box { height: 8rem; resize: none; } -.form-table .submit .button { - display: block; - width: 100%; -} .reply-mode { font-weight: bold; @@ -139,6 +135,10 @@ summary { margin-bottom: 0; } +.news { + margin: 8px 0; +} + .box { background-color: var(--box-color); border-right: 1px solid var(--box-border); @@ -213,6 +213,12 @@ summary { table-layout: fixed; } +.form-table .button, +.full-width { + display: block; + width: 100%; +} + .banner { display: block; width: 100%; diff --git a/templates/action.html b/templates/action.html index 4fdaf35..30fc3d3 100644 --- a/templates/action.html +++ b/templates/action.html @@ -4,6 +4,7 @@ {% block content %}
+

Výsledek

diff --git a/templates/banned.html b/templates/banned.html index 51a0691..85877e3 100644 --- a/templates/banned.html +++ b/templates/banned.html @@ -52,7 +52,7 @@ - diff --git a/templates/base.html b/templates/base.html index ba1c614..4c53d24 100755 --- a/templates/base.html +++ b/templates/base.html @@ -16,11 +16,18 @@ {% endblock %} diff --git a/templates/index.html b/templates/index.html index 8466a4b..559faac 100755 --- a/templates/index.html +++ b/templates/index.html @@ -18,12 +18,14 @@ diff --git a/templates/login.html b/templates/login.html index 3735568..7f4d12a 100755 --- a/templates/login.html +++ b/templates/login.html @@ -9,14 +9,14 @@
Výsledek
+
-

- {{ news.title }} - {{ news.author }} - {{ news.created|czech_datetime }} -

-
-
{{ news.content|safe }}
+
+

+ {{ news.title }} + {{ news.author }} - {{ news.created|czech_datetime }} +

+
+
{{ news.content|safe }}
+
- + - + - +
Jméno
Heslo
diff --git a/templates/macros/catalog-entry.html b/templates/macros/catalog-entry.html index a58c533..596d17d 100644 --- a/templates/macros/catalog-entry.html +++ b/templates/macros/catalog-entry.html @@ -18,6 +18,6 @@ {% endif %} -
{{ post.content|add_yous(post.board, tcx.yous)|safe }}
+
{{ post.content|add_yous(post.board, tcx.yous)|safe }}
{% endmacro %} diff --git a/templates/macros/post-actions.html b/templates/macros/post-actions.html index ebdebe6..7ec4ea6 100644 --- a/templates/macros/post-actions.html +++ b/templates/macros/post-actions.html @@ -40,7 +40,7 @@ - + - + {% endif %} - + - + Obsah - + Soubory
- 1 %} multiple="multiple"{% endif %}{% if (!reply && board.config.0.require_thread_file) || (reply && board.config.0.require_reply_file) %} required="required"{% endif %}> + 1 %} multiple="multiple"{% endif %}{% if (!reply && board.config.0.require_thread_file) || (reply && board.config.0.require_reply_file) %} required=""{% endif %}>
@@ -58,7 +58,7 @@ - + {% endif %} @@ -69,7 +69,7 @@ - + {% endif %} @@ -77,9 +77,9 @@ {% endif %} {% if reply %} - + {% else %} - + {% endif %} diff --git a/templates/macros/post.html b/templates/macros/post.html index 015f904..0ae6d07 100644 --- a/templates/macros/post.html +++ b/templates/macros/post.html @@ -57,6 +57,6 @@ {% endfor %} {% endif %} -
{{ post.content|add_yous(post.board, tcx.yous)|safe }}
+
{{ post.content|add_yous(post.board, tcx.yous)|safe }}
{% endmacro %} diff --git a/templates/macros/staff-nav.html b/templates/macros/staff-nav.html index 7da8429..fede065 100644 --- a/templates/macros/staff-nav.html +++ b/templates/macros/staff-nav.html @@ -18,5 +18,9 @@ {% if tcx.perms.owner() || tcx.perms.reports() %} [Hlášení] {% endif %} + + {% if tcx.perms.owner() || tcx.perms.news() %} + [Novinky] + {% endif %} {% endmacro %} diff --git a/templates/news.html b/templates/news.html new file mode 100644 index 0000000..71d10d0 --- /dev/null +++ b/templates/news.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}Novinky{% endblock %} + +{% block content %} +
+

Novinky

+ {% for newspost in news %} +
+

+ {{ newspost.title }} + {{ newspost.author }} - {{ newspost.created|czech_datetime }} +

+
+
{{ newspost.content|safe }}
+
+ {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/staff/account.html b/templates/staff/account.html index 790d433..2e67809 100755 --- a/templates/staff/account.html +++ b/templates/staff/account.html @@ -13,14 +13,14 @@ - + - + - +
Staré heslo
Nové heslo
@@ -31,18 +31,18 @@ - + - +
Účet
Potvrdit
- +
@@ -55,12 +55,12 @@ Potvrdit
- +
- + diff --git a/templates/staff/accounts.html b/templates/staff/accounts.html index fe9c890..a03e789 100755 --- a/templates/staff/accounts.html +++ b/templates/staff/accounts.html @@ -41,14 +41,14 @@ - + - + - +
Jméno
Heslo
diff --git a/templates/staff/banners.html b/templates/staff/banners.html index b9e32ee..1da830b 100755 --- a/templates/staff/banners.html +++ b/templates/staff/banners.html @@ -35,10 +35,10 @@ - + - +
Bannery
diff --git a/templates/staff/bans.html b/templates/staff/bans.html index 1716f3f..939dc03 100755 --- a/templates/staff/bans.html +++ b/templates/staff/bans.html @@ -26,7 +26,13 @@ {% for ban in bans %} - {{ ban.ip_range.network() }}-{{ ban.ip_range.broadcast() }} + + {% if ban.ip_range.network() == ban.ip_range.broadcast() %} + {{ ban.ip_range.ip() }} + {% else %} + {{ ban.ip_range.network() }}-{{ ban.ip_range.broadcast() }} + {% endif %} + {% if let Some(board) = ban.board %}/{{ board }}/{% else %}Všechny{% endif %}
{{ ban.reason }}
{{ ban.issued_by }} diff --git a/templates/staff/board-config.html b/templates/staff/board-config.html index c44ea67..56ddda4 100755 --- a/templates/staff/board-config.html +++ b/templates/staff/board-config.html @@ -23,32 +23,32 @@ Výchozí jméno - + Velikost stránky - + Počet stránek - + Limit souborů - + Limit naťuknutí - + Limit odpovědí - + @@ -156,21 +156,21 @@ Interval antispamu (IP) - + Interval antispamu (Obsah) - + Interval antispamu (IP+Obsah) - + - + diff --git a/templates/staff/boards.html b/templates/staff/boards.html index 27282a1..32d4fd3 100755 --- a/templates/staff/boards.html +++ b/templates/staff/boards.html @@ -43,14 +43,14 @@ - + - + - +
Jméno
Popis
{% endif %} @@ -61,18 +61,18 @@ - + - + - + - +
ID
Jméno
Popis
diff --git a/templates/staff/edit-news.html b/templates/staff/edit-news.html new file mode 100644 index 0000000..e348529 --- /dev/null +++ b/templates/staff/edit-news.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block title %}Upravit příspěvky{% endblock %} + +{% block content %} +
+

Upravit novinky

+
+
+ {% for newspost in news %} +
+

+ {{ newspost.title }} + {{ newspost.author }} - {{ newspost.created|czech_datetime }} +

+
+ +
+
+ {% endfor %} + +
+
+{% endblock %} diff --git a/templates/staff/logs.html b/templates/staff/logs.html deleted file mode 100755 index ff518c3..0000000 --- a/templates/staff/logs.html +++ /dev/null @@ -1,29 +0,0 @@ -{% import "../macros/pagination.html" as pagination %} -{% import "../macros/staff-nav.html" as staff_nav %} - -{% extends "base.html" %} - -{% block title %}Záznamy{% endblock %} - -{% block content %} -

Záznamy

-{% call staff_nav::staff_nav() %} -
-

Záznamy

-
- - - - - - {% for record in records %} - - - - - {% endfor %} -
ZprávaDatum
{{ record.message }}{{ record.created|czech_datetime }}
-
-
-{% call pagination::pagination("/staff/logs", pages, page) %} -{% endblock %} diff --git a/templates/staff/news.html b/templates/staff/news.html new file mode 100644 index 0000000..f3ea3c9 --- /dev/null +++ b/templates/staff/news.html @@ -0,0 +1,51 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Novinky{% endblock %} + +{% block content %} +

Novinky

+{% call staff_nav::staff_nav() %} +
+

Novinky

+
+
+ + + + + + + + {% for newspost in news %} + + + + + + + {% endfor %} +
TitulekAutorDatum
{{ newspost.title }}{{ newspost.author }}{{ newspost.created }}
+
+ + +
+
+

Vytvořit novinky

+
+ + + + + + + + + + + + +
Titulek
Obsah
+
+{% endblock %} diff --git a/templates/staff/permissions.html b/templates/staff/permissions.html index cf6ddfa..8c54cd5 100755 --- a/templates/staff/permissions.html +++ b/templates/staff/permissions.html @@ -43,6 +43,15 @@ + + Vlastní capcode + +
+ +
+ + + Záznamy @@ -88,6 +97,15 @@ + + Novinky + +
+ +
+ + + Obejít ban @@ -124,9 +142,18 @@ + + Obejít antispam + +
+ +
+ + + {% if tcx.perms.owner() %} - + {% endif %}