novinky + ostatní menší změny

Tento commit je obsažen v:
sneedmaster 2024-01-15 16:06:25 +01:00
rodič 2432fcba66
revize 01e0c8aeee
52 změnil soubory, kde provedl 700 přidání a 137 odebrání

76
Cargo.lock vygenerováno
Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

@ -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<Self, NekrochanError> {

Zobrazit soubor

@ -8,11 +8,13 @@ impl NewsPost {
ctx: &Ctx,
title: String,
content: String,
content_nomarkup: String,
author: String,
) -> Result<Self, NekrochanError> {
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?;

Zobrazit soubor

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

Zobrazit soubor

@ -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<Utc>) -> askama::Result<String> {
Ok(time)
}
pub fn czech_datetime(time: &DateTime<Utc>) -> askama::Result<String> {
pub fn czech_datetime(utc: &DateTime<Utc>) -> askama::Result<String> {
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();

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

@ -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,13 +106,14 @@ fn capcode_fallback(owner: bool) -> String {
pub async fn markup(
ctx: &Ctx,
board: &String,
board: Option<String>,
op: Option<i64>,
text: &str,
) -> Result<String, NekrochanError> {
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];
@ -139,6 +140,11 @@ pub async fn markup(
}
});
text.to_string()
} else {
text
};
let text = GREENTEXT_REGEX.replace_all(&text, "<span class=\"greentext\">&gt;$1</span>");
let text = ORANGETEXT_REGEX.replace_all(&text, "<span class=\"orangetext\">&lt;$1</span>");
let text = REDTEXT_REGEX.replace_all(&text, "<span class=\"redtext\">$1</span>");

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

@ -12,6 +12,7 @@ use crate::{
#[derive(Deserialize)]
pub struct EditPostsForm {
#[serde(default)]
pub posts: Vec<String>,
}

Zobrazit soubor

@ -32,7 +32,12 @@ pub async fn index(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, Nek
let boards = Board::read_all(&ctx).await?;
let stats = LocalStats::read(&ctx).await?;
let template = IndexTemplate { tcx, boards, stats, news };
let template = IndexTemplate {
tcx,
boards,
stats,
news,
};
template_response(&template)
}

Zobrazit soubor

@ -8,6 +8,7 @@ pub mod edit_posts;
pub mod index;
pub mod login;
pub mod logout;
pub mod news;
pub mod overboard;
pub mod overboard_catalog;
pub mod staff;

21
src/web/news.rs Normální soubor
Zobrazit soubor

@ -0,0 +1,21 @@
use actix_web::{get, web::Data, HttpRequest, HttpResponse};
use askama::Template;
use super::{tcx::TemplateCtx, template_response};
use crate::{ctx::Ctx, db::models::NewsPost, error::NekrochanError, filters};
#[derive(Template)]
#[template(path = "news.html")]
struct NewsTemplate {
tcx: TemplateCtx,
news: Vec<NewsPost>,
}
#[get("/news")]
pub async fn news(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
let tcx = TemplateCtx::new(&ctx, &req).await?;
let news = NewsPost::read_all(&ctx).await?;
let template = NewsTemplate { tcx, news };
template_response(&template)
}

Zobrazit soubor

@ -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<Ctx>,
req: HttpRequest,
QsForm(form): QsForm<CreateNewsForm>,
) -> Result<HttpResponse, NekrochanError> {
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)
}

70
src/web/staff/actions/edit_news.rs Normální soubor
Zobrazit soubor

@ -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<Ctx>,
req: HttpRequest,
QsForm(edits): QsForm<HashMap<i32, String>>,
) -> Result<HttpResponse, NekrochanError> {
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::<HashMap<i32, NewsPost>>();
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)
}

Zobrazit soubor

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

Zobrazit soubor

@ -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<i32>,
}
#[post("/staff/actions/remove-news")]
pub async fn remove_news(
ctx: Data<Ctx>,
req: HttpRequest,
QsForm(form): QsForm<RemoveNewsForm>,
) -> Result<HttpResponse, NekrochanError> {
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)
}

Zobrazit soubor

@ -13,15 +13,18 @@ pub struct UpdatePermissionsForm {
edit_posts: Option<String>,
manage_posts: Option<String>,
capcodes: Option<String>,
custom_capcodes: Option<String>,
staff_log: Option<String>,
reports: Option<String>,
bans: Option<String>,
banners: Option<String>,
news: Option<String>,
board_config: Option<String>,
bypass_bans: Option<String>,
bypass_board_lock: Option<String>,
bypass_thread_lock: Option<String>,
bypass_captcha: Option<String>,
bypass_antispam: Option<String>,
}
#[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?;

49
src/web/staff/edit_news.rs Normální soubor
Zobrazit soubor

@ -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<i32>,
}
#[derive(Template)]
#[template(path = "staff/edit-news.html")]
struct EditNewsTemplate {
tcx: TemplateCtx,
news: Vec<NewsPost>,
}
#[post("/staff/edit-news")]
pub async fn edit_news(
ctx: Data<Ctx>,
req: HttpRequest,
QsForm(form): QsForm<EditNewsForm>,
) -> Result<HttpResponse, NekrochanError> {
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)
}

Zobrazit soubor

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

25
src/web/staff/news.rs Normální soubor
Zobrazit soubor

@ -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<NewsPost>,
}
#[get("/staff/news")]
pub async fn news(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
let tcx = TemplateCtx::new(&ctx, &req).await?;
let news = NewsPost::read_all(&ctx).await?;
let template = NewsTemplate { tcx, news };
template_response(&template)
}

Zobrazit soubor

@ -14,7 +14,7 @@ use crate::{
pub struct TemplateCtx {
pub cfg: Cfg,
pub boards: Vec<String>,
pub logged_in: bool,
pub account: Option<String>,
pub perms: PermissionWrapper,
pub name: Option<String>,
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)

Zobrazit soubor

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

Zobrazit soubor

@ -4,6 +4,7 @@
{% block content %}
<div class="container">
<h1 class="title">Výsledek</h1>
<table class="data-table">
<tr><th>Výsledek</th></tr>
<tr>

Zobrazit soubor

@ -52,7 +52,7 @@
<td><textarea name="appeal"></textarea></td>
</tr>
<tr>
<td class="submit" colspan="2">
<td colspan="2">
<input class="button" type="submit" value="Odeslat">
</td>
</tr>

Zobrazit soubor

@ -16,11 +16,18 @@
<div class="board-links header">
<span class="link-group"><a href="/">domov</a></span>
{% call board_links::board_links() %}
<span class="link-group"><a href="/overboard">nadnástěnka</a></span>
<span class="link-group">
<a href="/overboard">nadnástěnka</a>
<span class="link-separator"></span>
<a href="/news">novinky</a></span>
</span>
<span class="float-r">
{% if tcx.logged_in %}
<span class="link-group"><a href="/logout">odhlásit se</a></span>
<span class="link-group"><a href="/staff/account">účet</a></span>
{% if tcx.account.is_some() %}
<span class="link-group">
<a href="/logout">odhlásit se</a>
<span class="link-separator"></span>
<a href="/staff/account">účet</a>
</span>
{% else %}
<span class="link-group"><a href="/login">přihlásit se</a></span>
{% endif %}

Zobrazit soubor

@ -10,11 +10,11 @@
{% for post in posts %}
<div class="box">
<b>Příspěvek #{{ post.id }} na /{{ post.board }}/</b>
<textarea class="edit-post" name="{{ post.board }}/{{ post.id }}">{{ post.content_nomarkup }}</textarea>
<textarea class="edit-box" name="{{ post.board }}/{{ post.id }}">{{ post.content_nomarkup }}</textarea>
</div>
<hr>
{% endfor %}
<input class="button" type="submit" value="Upravit">
<input class="button full-width" type="submit" value="Upravit">
</form>
</div>
{% endblock %}

Zobrazit soubor

@ -18,12 +18,14 @@
</tr>
<tr>
<td>
<div class="box">
<h2 class="headline">
<span>{{ news.title }}</span>
<span class="float-r">{{ news.author }} - {{ news.created|czech_datetime }}</span>
</h2>
<hr>
<pre class="post-content">{{ news.content|safe }}</pre>
<div class="post-content">{{ news.content|safe }}</div>
</div>
</td>
</tr>
<tr>

Zobrazit soubor

@ -9,14 +9,14 @@
<table class="form-table">
<tr>
<td class="label">Jméno</td>
<td><input name="username" type="text" required="required"></td>
<td><input name="username" type="text" required=""></td>
</tr>
<tr>
<td class="label">Heslo</td>
<td><input name="password" type="password" required="required"></td>
<td><input name="password" type="password" required=""></td>
</tr>
<tr>
<td class="submit" colspan="2"><input class="button" type="submit" value="Přihlásit se"></td>
<td colspan="2"><input class="button" type="submit" value="Přihlásit se"></td>
</tr>
</table>
</form>

Zobrazit soubor

@ -18,6 +18,6 @@
<img class="icon" src="/static/icons/locked.png">&#32;
{% endif %}
</b>
<pre class="post-content">{{ post.content|add_yous(post.board, tcx.yous)|safe }}</pre>
<div class="post-content">{{ post.content|add_yous(post.board, tcx.yous)|safe }}</div>
</div>
{% endmacro %}

Zobrazit soubor

@ -40,7 +40,7 @@
</td>
</tr>
<tr>
<td class="submit" colspan="2">
<td colspan="2">
<input
class="button"
type="submit"
@ -56,7 +56,7 @@
<td><textarea name="report_reason"></textarea></td>
</tr>
<tr>
<td class="submit" colspan="2">
<td colspan="2">
<input
class="button"
type="submit"
@ -159,7 +159,7 @@
</tr>
{% endif %}
<tr>
<td class="submit" colspan="2">
<td colspan="2">
<input
class="button"
type="submit"
@ -172,7 +172,7 @@
{% if tcx.perms.owner() || tcx.perms.edit_posts() %}
<table class="form-table">
<tr>
<td class="submit">
<td>
<input
class="button"
type="submit"

Zobrazit soubor

@ -25,14 +25,14 @@
<tr>
<td class="label">Obsah</td>
<td>
<textarea name="content" {% if (!reply && board.config.0.require_thread_content) || (reply && board.config.0.require_reply_content) %}required="required"{% endif %}></textarea>
<textarea name="content" {% if (!reply && board.config.0.require_thread_content) || (reply && board.config.0.require_reply_content) %}required=""{% endif %}></textarea>
</td>
</tr>
<tr>
<td class="label">Soubory</td>
<td>
<div class="input-wrapper">
<input name="files[]" type="file"{% if board.config.0.file_limit > 1 %} multiple="multiple"{% endif %}{% if (!reply && board.config.0.require_thread_file) || (reply && board.config.0.require_reply_file) %} required="required"{% endif %}>
<input name="files[]" type="file"{% if board.config.0.file_limit > 1 %} multiple="multiple"{% endif %}{% if (!reply && board.config.0.require_thread_file) || (reply && board.config.0.require_reply_file) %} required=""{% endif %}>
</div>
</td>
</tr>
@ -58,7 +58,7 @@
<td>
<img class="captcha" src="data:image/png;base64,{{ base64 }}">
<input name="captcha_id" type="hidden" value="{{ id }}">
<input name="captcha_solution" type="text" placeholder="Řešení" required="required">
<input name="captcha_solution" type="text" placeholder="Řešení" required="">
</td>
</tr>
{% endif %}
@ -69,7 +69,7 @@
<td>
<img class="captcha" src="data:image/png;base64,{{ base64 }}">
<input name="captcha_id" type="hidden" value="{{ id }}">
<input name="captcha_solution" type="text" placeholder="Řešení" required="required">
<input name="captcha_solution" type="text" placeholder="Řešení" required="">
</td>
</tr>
{% endif %}
@ -77,9 +77,9 @@
{% endif %}
<tr>
{% if reply %}
<td class="submit" colspan="2"><input class="button" type="submit" value="Nová odpověď"></td>
<td colspan="2"><input class="button" type="submit" value="Nová odpověď"></td>
{% else %}
<td class="submit" colspan="2"><input class="button" type="submit" value="Nové vlákno"></td>
<td colspan="2"><input class="button" type="submit" value="Nové vlákno"></td>
{% endif %}
</tr>
</table>

Zobrazit soubor

@ -57,6 +57,6 @@
{% endfor %}
</div>
{% endif %}
<pre class="post-content">{{ post.content|add_yous(post.board, tcx.yous)|safe }}</pre>
<div class="post-content">{{ post.content|add_yous(post.board, tcx.yous)|safe }}</div>
</div>
{% endmacro %}

Zobrazit soubor

@ -18,5 +18,9 @@
{% if tcx.perms.owner() || tcx.perms.reports() %}
<a href="/staff/reports">[Hlášení]</a>&#32;
{% endif %}
{% if tcx.perms.owner() || tcx.perms.news() %}
<a href="/staff/news">[Novinky]</a>&#32;
{% endif %}
</div>
{% endmacro %}

19
templates/news.html Normální soubor
Zobrazit soubor

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block title %}Novinky{% endblock %}
{% block content %}
<div class="container">
<h1 class="title">Novinky</h1>
{% for newspost in news %}
<div class="news box">
<h2 class="headline">
<span>{{ newspost.title }}</span>
<span class="float-r">{{ newspost.author }} - {{ newspost.created|czech_datetime }}</span>
</h2>
<hr>
<div class="post-content">{{ newspost.content|safe }}</div>
</div>
{% endfor %}
</div>
{% endblock %}

Zobrazit soubor

@ -13,14 +13,14 @@
<table class="form-table">
<tr>
<td class="label">Staré heslo</td>
<td><input name="old_password" type="password" required="required"></td>
<td><input name="old_password" type="password" required=""></td>
</tr>
<tr>
<td class="label">Nové heslo</td>
<td><input name="new_password" type="password" required="required"></td>
<td><input name="new_password" type="password" required=""></td>
</tr>
<tr>
<td class="submit" colspan="2"><input class="button" type="submit" value="Změnit heslo"></td>
<td colspan="2"><input class="button" type="submit" value="Změnit heslo"></td>
</tr>
</table>
</form>
@ -31,18 +31,18 @@
<table class="form-table">
<tr>
<td class="label">Účet</td>
<td><input name="account" type="text" required="required"></td>
<td><input name="account" type="text" required=""></td>
</tr>
<tr>
<td class="label">Potvrdit</td>
<td>
<div class="input-wrapper">
<input name="confirm" type="checkbox" required="required">
<input name="confirm" type="checkbox" required="">
</div>
</td>
</tr>
<tr>
<td class="submit" colspan="2"><input class="button" type="submit" value="Předat vlastnictví"></td>
<td colspan="2"><input class="button" type="submit" value="Předat vlastnictví"></td>
</tr>
</table>
</form>
@ -55,12 +55,12 @@
<td class="label">Potvrdit</td>
<td>
<div class="input-wrapper">
<input name="confirm" type="checkbox" required="required">
<input name="confirm" type="checkbox" required="">
</div>
</td>
</tr>
<tr>
<td class="submit" colspan="2"><input class="button" type="submit" value="Vymazat účet"></td>
<td colspan="2"><input class="button" type="submit" value="Vymazat účet"></td>
</tr>
</table>
</form>

Zobrazit soubor

@ -41,14 +41,14 @@
<table class="form-table">
<tr>
<td class="label">Jméno</td>
<td><input name="username" type="text" required="required"></td>
<td><input name="username" type="text" required=""></td>
</tr>
<tr>
<td class="label">Heslo</td>
<td><input name="password" type="password" required="required"></td>
<td><input name="password" type="password" required=""></td>
</tr>
<tr>
<td class="submit" colspan="2"><input class="button" type="submit" value="Vytvořit účet"></td>
<td colspan="2"><input class="button" type="submit" value="Vytvořit účet"></td>
</tr>
</table>
</form>

Zobrazit soubor

@ -35,10 +35,10 @@
<table class="form-table">
<tr>
<td class="label">Bannery</td>
<td><div class="input-wrapper"><input name="files[]" type="file" multiple="multiple" required="required"></div></td>
<td><div class="input-wrapper"><input name="files[]" type="file" multiple="multiple" required=""></div></td>
</tr>
<tr>
<td class="submit" colspan="2"><input class="button" type="submit" value="Přidat bannery"></td>
<td colspan="2"><input class="button" type="submit" value="Přidat bannery"></td>
</tr>
</table>
</form>

Zobrazit soubor

@ -26,7 +26,13 @@
{% for ban in bans %}
<tr>
<td><input name="bans[]" type="checkbox" value="{{ ban.id }}"></td>
<td title="{{ ban.ip_range }}">{{ ban.ip_range.network() }}-{{ ban.ip_range.broadcast() }}</td>
<td title="{{ ban.ip_range }}">
{% if ban.ip_range.network() == ban.ip_range.broadcast() %}
{{ ban.ip_range.ip() }}
{% else %}
{{ ban.ip_range.network() }}-{{ ban.ip_range.broadcast() }}
{% endif %}
</td>
<td>{% if let Some(board) = ban.board %}/{{ board }}/{% else %}<i>Všechny</i>{% endif %}</td>
<td><div class="post-content">{{ ban.reason }}</div></td>
<td>{{ ban.issued_by }}</td>

Zobrazit soubor

@ -23,32 +23,32 @@
<tr>
<td class="label">Výchozí jméno</td>
<td><input name="anon_name" type="text" value="{{ board.config.0.anon_name }}" required="required"></td>
<td><input name="anon_name" type="text" value="{{ board.config.0.anon_name }}" required=""></td>
</tr>
<tr>
<td class="label">Velikost stránky</td>
<td><input name="page_size" type="number" min="1" value="{{ board.config.0.page_size }}" required="required"></td>
<td><input name="page_size" type="number" min="1" value="{{ board.config.0.page_size }}" required=""></td>
</tr>
<tr>
<td class="label">Počet stránek</td>
<td><input name="page_count" type="number" min="1" value="{{ board.config.0.page_count }}" required="required"></td>
<td><input name="page_count" type="number" min="1" value="{{ board.config.0.page_count }}" required=""></td>
</tr>
<tr>
<td class="label">Limit souborů</td>
<td><input name="file_limit" type="number" min="1" value="{{ board.config.0.file_limit }}" required="required"></td>
<td><input name="file_limit" type="number" min="1" value="{{ board.config.0.file_limit }}" required=""></td>
</tr>
<tr>
<td class="label">Limit naťuknutí</td>
<td><input name="bump_limit" type="number" min="0" value="{{ board.config.0.bump_limit }}" required="required"></td>
<td><input name="bump_limit" type="number" min="0" value="{{ board.config.0.bump_limit }}" required=""></td>
</tr>
<tr>
<td class="label">Limit odpovědí</td>
<td><input name="reply_limit" type="number" min="0" value="{{ board.config.0.reply_limit }}" required="required"></td>
<td><input name="reply_limit" type="number" min="0" value="{{ board.config.0.reply_limit }}" required=""></td>
</tr>
<tr>
@ -156,21 +156,21 @@
<tr>
<td class="label">Interval antispamu (IP)</td>
<td><input name="antispam_ip" type="number" min="0" value="{{ board.config.0.antispam_ip }}" required="required"></td>
<td><input name="antispam_ip" type="number" min="0" value="{{ board.config.0.antispam_ip }}" required=""></td>
</tr>
<tr>
<td class="label">Interval antispamu (Obsah)</td>
<td><input name="antispam_content" type="number" min="0" value="{{ board.config.0.antispam_content }}" required="required"></td>
<td><input name="antispam_content" type="number" min="0" value="{{ board.config.0.antispam_content }}" required=""></td>
</tr>
<tr>
<td class="label">Interval antispamu (IP+Obsah)</td>
<td><input name="antispam_both" type="number" min="0" value="{{ board.config.0.antispam_both }}" required="required"></td>
<td><input name="antispam_both" type="number" min="0" value="{{ board.config.0.antispam_both }}" required=""></td>
</tr>
<tr>
<td class="submit" colspan="2"><input class="button" type="submit" value="Uložit"></td>
<td colspan="2"><input class="button" type="submit" value="Uložit"></td>
</tr>
</table>
</form>

Zobrazit soubor

@ -43,14 +43,14 @@
<table class="form-table">
<tr>
<td class="label">Jméno</td>
<td><input name="name" type="text" required="required"></td>
<td><input name="name" type="text" required=""></td>
</tr>
<tr>
<td class="label">Popis</td>
<td><input name="description" type="text" required="required"></td>
<td><input name="description" type="text" required=""></td>
</tr>
<tr>
<td class="submit" colspan="2"><input class="button" type="submit" formaction="/staff/actions/update-boards" value="Upravit vybrané"></td>
<td colspan="2"><input class="button" type="submit" formaction="/staff/actions/update-boards" value="Upravit vybrané"></td>
</tr>
</table>
{% endif %}
@ -61,18 +61,18 @@
<table class="form-table">
<tr>
<td class="label">ID</td>
<td><input name="id" type="text" required="required"></td>
<td><input name="id" type="text" required=""></td>
</tr>
<tr>
<td class="label">Jméno</td>
<td><input name="name" type="text" required="required"></td>
<td><input name="name" type="text" required=""></td>
</tr>
<tr>
<td class="label">Popis</td>
<td><input name="description" type="text" required="required"></td>
<td><input name="description" type="text" required=""></td>
</tr>
<tr>
<td class="submit" colspan="2"><input class="button" type="submit" value="Vytvořit nástěnku"></td>
<td colspan="2"><input class="button" type="submit" value="Vytvořit nástěnku"></td>
</tr>
</table>
</form>

24
templates/staff/edit-news.html Normální soubor
Zobrazit soubor

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}Upravit příspěvky{% endblock %}
{% block content %}
<div class="container">
<h1 class="title">Upravit novinky</h1>
<hr>
<form method="post" action="/staff/actions/edit-news">
{% for newspost in news %}
<div class="box">
<h2 class="headline">
<span>{{ newspost.title }}</span>
<span class="float-r">{{ newspost.author }} - {{ newspost.created|czech_datetime }}</span>
</h2>
<hr>
<textarea class="edit-box" name="{{ newspost.id }}">{{ newspost.content_nomarkup }}</textarea>
</div>
<hr>
{% endfor %}
<input class="button full-width" type="submit" value="Upravit">
</form>
</div>
{% endblock %}

Zobrazit soubor

@ -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 %}
<h1 class="title">Záznamy</h1>
{% call staff_nav::staff_nav() %}
<hr>
<h2>Záznamy</h2>
<div class="table-wrap">
<table class="data-table center">
<tr>
<th>Zpráva</th>
<th>Datum</th>
</tr>
{% for record in records %}
<tr>
<td>{{ record.message }}</td>
<td>{{ record.created|czech_datetime }}</td>
</tr>
{% endfor %}
</table>
</div>
<hr>
{% call pagination::pagination("/staff/logs", pages, page) %}
{% endblock %}

51
templates/staff/news.html Normální soubor
Zobrazit soubor

@ -0,0 +1,51 @@
{% import "../macros/staff-nav.html" as staff_nav %}
{% extends "base.html" %}
{% block title %}Novinky{% endblock %}
{% block content %}
<h1 class="title">Novinky</h1>
{% call staff_nav::staff_nav() %}
<hr>
<h2>Novinky</h2>
<form method="post">
<div class="table-wrap">
<table class="data-table center">
<tr>
<th></th>
<th>Titulek</th>
<th>Autor</th>
<th>Datum</th>
</tr>
{% for newspost in news %}
<tr>
<td><input name="news[]" type="checkbox" value="{{ newspost.id }}"></td>
<td>{{ newspost.title }}</td>
<td>{{ newspost.author }}</td>
<td>{{ newspost.created }}</td>
</tr>
{% endfor %}
</table>
</div>
<input class="button" type="submit" formaction="/staff/actions/remove-news" value="Odstranit vybrané">&#32;
<input class="button" type="submit" formaction="/staff/edit-news" value="Upravit vybrané">
</form>
<hr>
<h2>Vytvořit novinky</h2>
<form method="post" action="/staff/actions/create-news">
<table class="form-table">
<tr>
<td class="label">Titulek</td>
<td><input name="title" type="text" required=""></td>
</tr>
<tr>
<td class="label">Obsah</td>
<td><textarea name="content" required=""></textarea></td>
</tr>
<tr>
<td colspan="2"><input class="button" type="submit" value="Vytvořit novinky"></td>
</tr>
</table>
</form>
{% endblock %}

Zobrazit soubor

@ -43,6 +43,15 @@
</td>
</tr>
<tr>
<td class="label">Vlastní capcode</td>
<td>
<div class="input-wrapper">
<input name="custom_capcodes" type="checkbox"{% if account.perms().custom_capcodes() %} checked="checked"{% endif %}{% if !tcx.perms.owner() %} disabled=""{% endif %}>
</div>
</td>
</tr>
<tr>
<td class="label">Záznamy</td>
<td>
@ -88,6 +97,15 @@
</td>
</tr>
<tr>
<td class="label">Novinky</td>
<td>
<div class="input-wrapper">
<input name="news" type="checkbox"{% if account.perms().news() %} checked="checked"{% endif %}{% if !tcx.perms.owner() %} disabled=""{% endif %}>
</div>
</td>
</tr>
<tr>
<td class="label">Obejít ban</td>
<td>
@ -124,9 +142,18 @@
</td>
</tr>
<tr>
<td class="label">Obejít antispam</td>
<td>
<div class="input-wrapper">
<input name="bypass_antispam" type="checkbox"{% if account.perms().bypass_antispam() %} checked="checked"{% endif %}{% if !tcx.perms.owner() %} disabled=""{% endif %}>
</div>
</td>
</tr>
{% if tcx.perms.owner() %}
<tr>
<td class="submit" colspan="2"><input class="button" type="submit" value="Uložit"></td>
<td colspan="2"><input class="button" type="submit" value="Uložit"></td>
</tr>
{% endif %}
</table>